Full Width [alt+shift+f] Shortcuts [alt+shift+k]
Sign Up [alt+shift+s] Log In [alt+shift+l]
25

Food vacation in Japan

from Jonas Hietala [alt+shift+b] in technology

Me and my friend spent 2 weeks in Japan and had a great time. I wasn’t planning on taking a lot of pictures, but I ended up sending a bunch back home as a ways of keeping my family up to date. When I look back most of them are about food… So I’ll turn this into a post about the various foods we ate in Japan! Breakfast and dinner at an Onsen. Japan has this supposedly famous breakfast, it’s more like a dinner than what we in Sweden call breakfast. And sure enough we got to eat our bellies full. I don’t usually eat breakfast, but this was actually pretty nice. Maybe it was because we slept almost 12 hours after our trip? We also had dinner with a lot of different courses (I only took one picture though). It was also very good but I don’t really know what we ate… What I ate at a Maid café. The ketchup is supposed to be Yotsuba, but I don’t see it. And of course we had to visit a Maid café. The whole experience as extremely weird… Unfortunately the food wasn’t that great either, it was the only time I was disappointed of the food in the whole trip (except the worthless airline food). Wagyu. Japanese beef. We ate some kind of local meat—Wagyu—at a fancy restaurant. It was extremely good. We ate a lot of great food during our trip but this was my favorite. Of course it was expensive, but you should still consider it if you ever go to Japan. Yakiniku, meat we got to grill ourselves. My friend was really hyped to go to an all-you-can-eat place where you grill your meat yourself. And for good reason—it was amazing. I love meat, and the meat was great, and I enjoyed the process of grilling the meat. Also a must if you ever go to Japan IMHO. Shabu-shabu. A hot pot where we dumped meat and vegetables then fished them out and ate them. Another similar concept is Shabu-shabu. Instead of grilling the meat you drop it into a pot with boiling water and fish it up after a few seconds. It was also very good, but not as great as Yakiniku. Assortment of sushi and...
18th Jul 2019

Stay updated

Get a weekly newsletter with the top 5 articles worth reading every week.

More from Jonas Hietala

Super ultrawide Niri

A Niri workspace with 7 visible columns. After having used practically the same xmonad configuration for a decade and a half I’ve now modernized my setup with the scrollable-tiling Wayland compositor Niri. It’s been a bit of a struggle to unlearn my old workflow but I’m really growing to love Niri’s scrollable workflow, especially on my new super ultrawide display. Samsung Odyssey Neo G9 G95NC 57” My new 57” single monitor setup. What kicked off my Niri journey was the purchase of a new super ultrawide monitor. I bought the 57” Odyssey Neo G9 as it was the largest monitor I could find. (It’s marketed as a “gaming” display but it’s really an amazing productivity display.) It replaced my old 3-monitor setup: My old 3-monitor setup. The new display is wider so I had to move the speakers around 10–15cm further apart. I was debating whether to replace the center 31.5” monitor or replace all monitors with a single one but I think I made the right choice with the ultrawide. The curvature wasn’t an issue (I’ve come to prefer it) and the extra vertical space the portrait side monitors provided wasn’t as crucial as I thought. I think an ultrawide is worth it just to get rid of the annoying bezels. Small things can be a big thing sometimes. A more dynamic workflow xmonad and Niri are similar yet different. Both automatically lay out windows as you spawn them but xmonad (at least the way I used it) follows a layout algorithm that re-flows using a “master” window and combines the rest of the windows into one space, while Niri lays out windows in columns. The change is subtle but it implies that a new window won’t change the size of other windows. This is very nice if you spawn a lot of short-lived terminals or web browsers like I do and it reduces the amount of manual reshuffling I spend time on. My xmonad workflow was more static than my Niri one. In xmonad I made heavy use of workspaces, mapping ten workspaces mentally to different programs, such as 0 Firefox and 1 terminal logs on the left monitor; 3, 4 and 5 for different Neovim instances on the center monitor; 8 as chat and 9 for music or video on the right monitor. I had no rules to enforce this; it’s an emergent behaviour that served me well for years. With Niri it’s more dynamic. I still use workspaces but they no longer have direct shortcuts, I simply go up/down in the workspace list. Maybe I’ll add them in the future but with 3–4 workspaces that’s not as necessary. I spawn workspaces/windows when I need them and remove them when I’m done. Usually it’s one workspace per project (yes, I’m now one of those who have multiple up at once) with all the related things such as editor, terminals, and browser with docs. I don’t typically utilize the full screen width and I try to keep the things I’m working on in the center, often leaving 10–30% gaps on the sides. Even though I don’t normally use the “endless scrolling” feature of Niri I re-center selected windows all the time so I can look straight ahead as much as possible. Keyboard shortcuts As a fan of keyboard layouts of course I have to spend some time tinkering with good keyboard shortcuts (especially as Niri’s recommended keybinds don’t map well with my custom keyboard or custom layout). Navigation layer What I did was add a new navigation layer that’s enabled by holding Tab (ring + middle + index on the left-hand side) with all Niri related movement and layout keybinds. In the graphics above, all green-colored keys emit Gui (which gates all window manager commands) and you can see: Long press on Close Window to close a window. The long press requirement prevents accidentally closing windows. Arrows move through columns/windows. Long press resizes them. Workspace Up/Down focuses a different workspace. Center a column. Consume/Expel to combine windows into one column. (consume-or-expel-window-left/consume-or-expel-window-right) Expand Column makes a column take up all remaining space. (expand-column-to-available-width) Audio controls. To press them I release the index finger (keeping the ring and middle finger pressed to keep the layer active) and use the index to press the audio buttons. Mouse buttons. In Niri you can move floating windows with Gui + Left Mouse and Gui + Right Mouse to resize them. As my main mouse is a trackball integrated into the keyboard I had to add them to the left-hand side. I ended up using QMK’s customizable key repress feature that allows me to: Tab combo with my three fingers (layer is active) Release only the index (layer is still active, same as with the audio controls) Press the index again (now detects the press Gui + Left Mouse key down) Use the trackball to move the window And similarly for the right mouse button to resize with the middle finger. Works great! Because there are so many commands I want to send I placed Ctrl on the thumb that provides movement-related commands like so: For example: Arrows move columns/windows in the four directions. Move columns to the neighboring workspaces. Center visible columns. (center-visible-columns) Slightly different consume/expel semantics. (consume-window-into-column/expel-window-from-column) Regular keymaps These are triggered in the “normal” way by first pressing the Super combo and then another key on the base layer (I use autoshift so I shift with a long press). Window management Super + F toggle windowed fullscreen (keep column width) Super + Shift + F fullscreen window (over the entire display) Super + M maximize column (moves other columns) Run stuff Super + Enter terminal Super + E Noctalia’s launcher (also exists on the navigation layer as Launch) Super + S show Noctalia control center Super + Shift + S show Noctalia settings Super + Q power off monitors (they wake on input) Super + Shift + Q show Noctalia session menu (reboot etc) Super + Shift + L lock screen Misc Super + H show hotkey overlay Super + P interactive screenshot Super + Shift + P screenshot selected window Tweaks to the standard CachyOS setup In the process of moving from xmonad to Niri I also moved from Void Linux to CachyOS and I let the installer install Niri and give me a basic configuration together with Noctalia (that provides a statusbar, notifications, and a bunch of things you apparently need). Center the status bar and other Noctalia windows My centered Noctalia status bar. Feels absolutely required on this screen otherwise things end up in the corners. Firefox on XWayland Force Firefox onto XWayland as the Wayland popup manager is broken: environment { MOZ_ENABLE_WAYLAND "0" } Dead keys for Ghostty For some reason dead keys were broken in Ghostty. This is bad for me as the OS keyboard is set to Swedish and it uses them to type ~ (quite a crucial character for a programmer). The fix: environment { GTK_IM_MODULE "ibus" QT_IM_MODULE "ibus" XMODIFIERS "@im=ibus" } This needs ibus installed and running. Melange colorscheme Noctalia discovers custom color schemes under ~/.config/noctalia/colorschemes/<Name>/<Name>.json, so I dropped in my trusty Melange colorscheme there: { "dark": { "mPrimary": "#EBC06D", "mOnPrimary": "#292522", "mSecondary": "#A3A9CE", "mOnSecondary": "#292522", "mTertiary": "#85B695", "mOnTertiary": "#292522", "mError": "#D47766", "mOnError": "#292522", "mSurface": "#292522", "mOnSurface": "#ECE1D7", "mSurfaceVariant": "#34302C", "mOnSurfaceVariant": "#C1A78E", "mOutline": "#867462", "mShadow": "#1a1816", "mHover": "#E49B5D", "mOnHover": "#292522", "terminal": { "normal": { "black": "#867462", "red": "#D47766", "green": "#85B695", "yellow": "#EBC06D", "blue": "#A3A9CE", "magenta": "#CF9BC2", "cyan": "#89B3B6", "white": "#ECE1D7" }, "bright": { "black": "#34302C", "red": "#BD8183", "green": "#78997A", "yellow": "#E49B5D", "blue": "#7F91B2", "magenta": "#B380B0", "cyan": "#7B9695", "white": "#C1A78E" }, "foreground": "#ECE1D7", "background": "#292522", "selectionFg": "#C1A78E", "selectionBg": "#403A36", "cursorText": "#292522", "cursor": "#EBC06D" } } } Then pick the colorscheme: "colorSchemes": { "darkMode": true, "predefinedScheme": "Melange", "useWallpaperColors": false } Layout appearance The default appearance was pretty I admit but way too much blank space and weirdness. Some tweaks: layout { // Required for noctalia-shell to set wallpaper background-color "transparent" // Never auto-center focused columns (too much movement) center-focused-column "never" // But do center a single window always-center-single-column // No extra space around it all struts {} // No gaps between windows gaps 0 // The focus ring was annoying focus-ring { off } // Use a border with consistent width for all windows instead border { on width 2 active-color "#ebc06d" inactive-color "#403a36" } // Setting widths is important with such a large screen preset-column-widths { proportion 0.15 proportion 0.3 proportion 0.4 } default-column-width { proportion 0.15; } // Heights too, why not? preset-window-heights { proportion 0.15 proportion 0.5 proportion 1.0 } } // Prevent the mouse from opening the overview in the corners gestures { hot-corners { off } } Keep windows centered Niri has the always-center-single-column option, which is nice as I want to keep as much as possible in the center of the monitor when I’m working. But I very frequently use 2–3 smaller windows and with my frequent opening and closing I’d like them centered too. Luckily, Niri has an IPC you can use to make a small program that reacts to events and does this for you. I made a small rust project using the niri-ipc crate that does this for me: The autocenter implementation [dependencies] niri-ipc = "26.4.0" use std::collections::HashMap; use std::io; use niri_ipc::socket::Socket; use niri_ipc::{Action, Event, Request, Response, Window, Workspace}; #[derive(Clone, Copy, PartialEq, Eq, Hash)] struct WindowId(u64); #[derive(Clone, Copy, PartialEq, Eq)] struct WorkspaceId(u64); struct OutputName<'a>(&'a str); struct WindowState { workspace: Option<WorkspaceId>, width: f64, } fn main() -> io::Result<()> { let mut socket = Socket::connect()?; if !matches!(socket.send(Request::EventStream)?, Ok(Response::Handled)) { eprintln!("niri rejected event stream"); std::process::exit(1); } let mut known: HashMap<WindowId, WindowState> = HashMap::new(); let mut read_event = socket.read_events(); loop { let result = match read_event()? { // A full snapshot of the current state. Just refresh our state. Event::WindowsChanged { windows } => { known = windows .into_iter() .map(|w| { ( WindowId(w.id), WindowState { workspace: w.workspace_id.map(WorkspaceId), width: w.layout.tile_size.0, }, ) }) .collect(); Ok(()) } Event::WindowOpenedOrChanged { window } => { let workspace = window.workspace_id.map(WorkspaceId); let entry = WindowState { workspace, width: window.layout.tile_size.0, }; let prev = known.insert(WindowId(window.id), entry); match prev { // Don't center floats. _ if window.is_floating => Ok(()), // New window, try to re-center. None => maybe_center_new(&window), // Window changed workspace, try to re-center. Some(state) if state.workspace != workspace => center_focused_if_fits(), // Skip other things. Some(_) => Ok(()), } } Event::WindowClosed { id } => { if known.remove(&WindowId(id)).is_some() { center_focused_if_fits() } else { Ok(()) } } Event::WindowLayoutsChanged { changes } => { // Only re-center if the width was changed, otherwise our re-center will // loop back indefinitely. let mut resized = false; for (id, layout) in changes { if let Some(state) = known.get_mut(&WindowId(id)) { if (state.width - layout.tile_size.0).abs() > 0.5 { state.width = layout.tile_size.0; resized = true; } } } if resized { center_focused_if_fits() } else { Ok(()) } } _ => Ok(()), }; if let Err(e) = result { eprintln!("autocenter: {e}"); } } } /// Center a newly created window if the workspace is focused and if there's surrounding free space left. fn maybe_center_new(window: &Window) -> io::Result<()> { let Some(workspace_id) = window.workspace_id.map(WorkspaceId) else { return Ok(()); }; let Some(focused) = focused_workspace()? else { return Ok(()); }; if WorkspaceId(focused.id) == workspace_id { center_if_fits(&focused)?; } Ok(()) } /// Center windows in the focused workspace if there's surrounding free space left. fn center_focused_if_fits() -> io::Result<()> { if let Some(focused) = focused_workspace()? { center_if_fits(&focused)?; } Ok(()) } /// Center windows in the workspace if there's surrounding free space left. fn center_if_fits(workspace: &Workspace) -> io::Result<()> { let Some(output) = workspace.output.as_deref().map(OutputName) else { return Ok(()); }; let Some(width) = output_width(output)? else { return Ok(()); }; if workspace_width(WorkspaceId(workspace.id))? < f64::from(width) { center_visible_columns()?; } Ok(()) } /// Issue a one-shot query to Niri, wait, and return the response. fn query(request: Request) -> io::Result<Response> { match Socket::connect()?.send(request)? { Ok(response) => Ok(response), Err(msg) => Err(io::Error::other(msg)), } } /// Get the focused workspace. fn focused_workspace() -> io::Result<Option<Workspace>> { match query(Request::Workspaces)? { Response::Workspaces(ws) => Ok(ws.into_iter().find(|w| w.is_focused)), _ => Ok(None), } } /// Get the width of an output (monitor). fn output_width(name: OutputName<'_>) -> io::Result<Option<u32>> { match query(Request::Outputs)? { Response::Outputs(outputs) => Ok(outputs .get(name.0) .and_then(|o| o.logical.as_ref()) .map(|l| l.width)), _ => Ok(None), } } /// Calculates the width of all columns in the workspace. fn workspace_width(workspace_id: WorkspaceId) -> io::Result<f64> { let Response::Windows(windows) = query(Request::Windows)? else { return Ok(0.0); }; let mut columns: HashMap<usize, f64> = HashMap::new(); for w in windows { if w.workspace_id.map(WorkspaceId) != Some(workspace_id) { continue; } if let Some((col, _)) = w.layout.pos_in_scrolling_layout { let width = columns.entry(col).or_insert(0.0); *width = width.max(w.layout.tile_size.0); } } Ok(columns.values().sum()) } /// Send a command to center the visible columns. fn center_visible_columns() -> io::Result<()> { if let Err(msg) = Socket::connect()?.send(Request::Action(Action::CenterVisibleColumns {}))? { eprintln!("center-visible-columns rejected: {msg}"); } Ok(()) } One catch is that if a new window overflows the monitor width, the script won’t center the columns even if there would be free space left afterwards. This is a little weird but it’s consistent with Niri’s center-visible-columns command. I had a small itch to try to hack around it but in the end I left it alone… Is Niri worth it? Yes, absolutely. Niri has been a huge upgrade for me in combination with a single wide screen. My xmonad setup worked really well with three monitors—arguably a better fit in that context than Niri—but for the big-screen use-case Niri is superior. I’m curious how it holds up on my laptop, once I gather enough energy to install CachyOS on it… But that’s a side quest. The big-screen setup I spend most of my days in is the best I’ve ever had, and I have no desire to go back.

11th Sep 2026 • 2 votes
Domains, certificates, and DNS

Accessing services via raw IP addresses isn’t that swell; I’m no Rain Man. It’s time to set up subdomains for my hietala.xyz domain for internal use. We’ll use the Gateway API to set up routes, cert-manager to give us https without self-signed browser warnings, and ExternalDNS to set up DNS overrides. HTTP routing I believe the flow of resolving http://argocd.hietala.xyz to a service looks something like this: digraph { rankdir=LR "Browser" -> "OPNsense DNS" [label="resolve host"] "OPNsense DNS" -> "Browser" [label="Gateway IP"] "Browser" -> "Cilium Gateway" [label="HTTP request" class="label-below"] "Cilium Gateway" -> "HTTPRoute" [label="match hostname" class="label-below"] "HTTPRoute" -> "Service" [label="backendRef" class="label-below"] "Service" -> "Pod" [label="EndpointSlice" class="label-below"] } First the browser asks my router running OPNsense about argocd.hietala.xyz and gets the Gateway IP address. The request then flows through a route (http or https), to a Service, and eventually a Pod where ArgoCD is running. We’ll handle these one at a time but let’s start by creating the Gateway and giving it an IP (we’ll use 10.1.4.101): apiVersion: gateway.networking.k8s.io/v1 kind: Gateway metadata: name: main namespace: kube-system annotations: io.cilium/lb-ipam-ips: "10.1.4.101" spec: gatewayClassName: cilium We installed the prerequisites for Gateway when we configured Cilium during our initial cluster bootstrap. Then we need to tell the Gateway to manage all http routes for all namespaces (we’ll get back to https): # Continued from the above Gateway manifest spec: gatewayClassName: cilium listeners: - name: http port: 80 protocol: HTTP allowedRoutes: namespaces: from: All Then we can add an HTTPRoute for our ArgoCD application that targets argocd-server at port 80: # Below the ArgoCD Application setup # Resources in the same file are separated by `---` --- apiVersion: gateway.networking.k8s.io/v1 kind: HTTPRoute metadata: name: argocd namespace: argocd spec: parentRefs: - name: main namespace: kube-system sectionName: http hostnames: - argocd.hietala.xyz rules: - backendRefs: - name: argocd-server port: 80 If we then add a DNS override from http://argocd.hietala.xyz to 10.1.4.101 (/etc/hosts or OPNsense or similar) then we should be able to reach http://argocd.hietala.xyz. Enabling SSL I want https://argocd.hietala.xyz to “just work” and for that we need to tell Gateway to manage https routes: # ... spec: gatewayClassName: cilium listeners: - name: https port: 443 protocol: HTTPS tls: mode: Terminate certificateRefs: - name: hietala-xyz-tls allowedRoutes: namespaces: from: All - name: http # Http definition from before (Note the tls addition that terminates using a not-yet-defined certificate.) And add the https route itself: apiVersion: gateway.networking.k8s.io/v1 kind: HTTPRoute metadata: name: argocd namespace: argocd spec: parentRefs: - name: main namespace: kube-system sectionName: https hostnames: - argocd.hietala.xyz rules: - backendRefs: - name: argocd-server port: 80 This doesn’t work just yet as we need to create the hietala-xyz-tls cert. Cert-manager cert-manager seems like the standard way to manage certificates for Kubernetes. I don’t want to expose my services to the internet which means I need a DNS01 challenge. cert-manager doesn’t natively support Namecheap (bummer) but there’s an open source webhook Namecheap provider out there. It hasn’t been updated in a couple of years but I couldn’t find an alternative… YOLO I guess? The cert-manager manifest: apiVersion: argoproj.io/v1alpha1 kind: Application metadata: name: cert-manager namespace: argocd annotations: argocd.argoproj.io/sync-wave: "-1" finalizers: - resources-finalizer.argocd.argoproj.io spec: project: default source: repoURL: https://charts.jetstack.io chart: cert-manager targetRevision: v1.21.0 helm: values: | installCRDs: true destination: server: https://kubernetes.default.svc namespace: cert-manager syncPolicy: syncOptions: - CreateNamespace=true automated: prune: true selfHeal: true Note the installCRDs: true that makes things easier for us, and CreateNamespace=true which will create the cert-manager namespace for us too. Saves some typing. We also set sync-wave to -1 as it needs to sync before the cluster issuer and certificate that we’ll define later. Then the namecheap webhook: apiVersion: argoproj.io/v1alpha1 kind: Application metadata: name: cert-manager-webhook-namecheap namespace: argocd annotations: argocd.argoproj.io/sync-wave: "-1" finalizers: - resources-finalizer.argocd.argoproj.io spec: project: default source: repoURL: https://github.com/kelvie/cert-manager-webhook-namecheap.git targetRevision: HEAD path: deploy/cert-manager-webhook-namecheap helm: # Identifier that our issuer will use values: | groupName: acme.namecheap.com destination: server: https://kubernetes.default.svc namespace: cert-manager syncPolicy: syncOptions: - CreateNamespace=false automated: prune: true selfHeal: true Then a ClusterIssuer that uses the namecheap webhook: apiVersion: cert-manager.io/v1 kind: ClusterIssuer metadata: name: letsencrypt-staging annotations: # Must come after the cert-manager applications. # It defaults to 0 anyway but this is more explicit. argocd.argoproj.io/sync-wave: "0" # If resources doesn't exist ArgoCD may complain. argocd.argoproj.io/sync-options: SkipDryRunOnMissingResource=true spec: acme: # Use the staging endpoint during testing! server: https://acme-staging-v02.api.letsencrypt.org/directory email: [email protected] privateKeySecretRef: name: letsencrypt-staging solvers: - dns01: # Use the namecheap webhook webhook: # Same identifier the webhook defined above. groupName: acme.namecheap.com solverName: namecheap config: # These are very sensitive! # Store them in a Sealed Secret apiKeySecretRef: name: namecheap-credentials key: apiKey apiUserSecretRef: name: namecheap-credentials key: apiUser Make sure to use the staging issuer during testing to avoid rate limits. When you’re done playing around you can switch to the production server at https://acme-v02.api.letsencrypt.org/directory. I created a new ClusterIssuer called letsencrypt-prod instead of replacing the url so I can easily change between them if I need to. Create the sealed secret: kubectl create secret generic namecheap-credentials \ --namespace cert-manager \ --from-literal=apiKey="key" \ --from-literal=apiUser="user" \ --dry-run=client -o yaml \ | kubeseal --cert infrastructure/sealed-secrets-cert.pem \ --format yaml \ > gitops/apps/cert-manager/namecheap-secret.yaml Finally we need to create the hietala-xyz-tls certificate that uses the issuer: apiVersion: cert-manager.io/v1 kind: Certificate metadata: name: hietala-xyz # Must match the namespace of the Gateway namespace: kube-system annotations: # References the issuer so must be synced after. argocd.argoproj.io/sync-wave: "2" argocd.argoproj.io/sync-options: SkipDryRunOnMissingResource=true spec: # Gateway references this certificate using this name. secretName: hietala-xyz-tls dnsNames: - "*.hietala.xyz" - "hietala.xyz" issuerRef: # Switch to `letsencrypt-prod` later. name: letsencrypt-staging kind: ClusterIssuer When all this has been synced we should be able to see that the certificate is created: $ kubectl get certificate -n kube-system NAME READY SECRET AGE hietala-xyz True hietala-xyz-tls 4d20h And that we can visit https://argocd.hietala.xyz (browser will warn while we use letsencrypt-staging, on prod it should be without errors). (If not, then you have a bunch of debugging to do. Have fun!) Want to redirect http to https? Add this route: apiVersion: gateway.networking.k8s.io/v1 kind: HTTPRoute metadata: name: argocd-redirect namespace: argocd spec: parentRefs: - name: main namespace: kube-system sectionName: http hostnames: - argocd.hietala.xyz rules: - filters: - type: RequestRedirect requestRedirect: scheme: https statusCode: 301 In general though I skip the http route as I tell Firefox to always use https. ArgoCD is still accessible via its external IP I defined in a previous post but you can also port-forward or proxy to it: # http://localhost:8001/api/v1/namespaces/argocd/services/argocd-server:80/proxy/ kubectl proxy # localhost:8123 kubectl port-forward svc/argocd-server -n argocd 8123:80 Automating DNS overrides By now most of the things are set up in proper GitOps fashion but there’s still one thing I have to do manually: I have to add a DNS override to unbound (it’s on my OPNsense router). Doing it once is fine but it gets old fast. Some of my overrides in unbound. Can you see a pattern? A wildcard domain could work but I have other services running outside of Kubernetes, so I’d like a cleaner solution. That solution is ExternalDNS, which automatically adds overrides for any existing Gateway HTTPRoute. There’s a webhook provider for OPNsense that we’ll use. The manifest: apiVersion: argoproj.io/v1alpha1 kind: Application metadata: name: external-dns namespace: argocd finalizers: - resources-finalizer.argocd.argoproj.io spec: project: default sources: - repoURL: https://kubernetes-sigs.github.io/external-dns/ chart: external-dns targetRevision: 1.21.1 helm: valueFiles: - $values/gitops/apps/external-dns/values.yaml - repoURL: https://git.hietala.xyz/tree/home-ops.git targetRevision: HEAD ref: values destination: server: https://kubernetes.default.svc namespace: external-dns syncPolicy: syncOptions: - CreateNamespace=true automated: prune: true selfHeal: true It loads values.yaml from the repo: provider: name: webhook webhook: image: repository: ghcr.io/crutonjohn/external-dns-opnsense-webhook tag: v1.0.0 env: - name: OPNSENSE_HOST value: "https://router.hietala.xyz" # Remember to create the `external-dns` sealed secret. - name: OPNSENSE_API_KEY valueFrom: secretKeyRef: name: external-dns key: api-key - name: OPNSENSE_API_SECRET valueFrom: secretKeyRef: name: external-dns key: api-secret sources: - gateway-httproute policy: sync domainFilters: - hietala.xyz txtOwnerId: talos-dorne This connects to OPNsense, sources routes from the Gateway, targets the hietala.xyz domain, and uses a new external-dns secret: kubectl create secret generic external-dns \ --namespace external-dns \ --from-literal=api-key="key" \ --from-literal=api-secret="secret" \ --dry-run=client -o yaml \ | kubeseal --cert infrastructure/sealed-secrets-cert.pem \ --format yaml \ > gitops/apps/external-dns/external-dns-secret.yaml With this I don’t have to add manual overrides anymore. Nothing like spending hours to automate a few minutes of work! If you want to extend support to other network things, say a Minecraft server talking TCP over 25565 with a fixed IP, we can add service support to ExternalDNS: sources: - gateway-httproute - service And declare the Minecraft Service like so: apiVersion: v1 kind: Service metadata: name: minecraft annotations: external-dns.alpha.kubernetes.io/hostname: mc.hietala.xyz spec: type: ClusterIP externalIPs: - 10.1.4.53 selector: app: minecraft ports: - port: 25565 targetPort: 25565 protocol: TCP name: minecraft Many steps but the end is nice There’s a few moving parts but once done supporting new apps is satisfyingly easy. For example, to expose the homeassistant service under https://ha.hietala.xyz this is enough: apiVersion: gateway.networking.k8s.io/v1 kind: HTTPRoute metadata: name: homeassistant spec: parentRefs: - name: main namespace: kube-system sectionName: https hostnames: - ha.hietala.xyz rules: - backendRefs: - name: homeassistant port: 8123 Commit and push, and https://ha.hietala.xyz is ready in a jiffy, certificates and DNS overrides included. I encountered a bug in ArgoCD v3.3.5 where it’ll get stuck syncing the routes with extra lines in the diff (or mark them as out of sync), for example here: - backendRefs: - name: my-app port: 80 group: "" kind: Service weight: 1 matches: - path: type: PathPrefix value: / You can add the lines to each route to fix it but you can also tell ArgoCD to ignore them with these lines in the Application template: apiVersion: argoproj.io/v1alpha1 kind: ApplicationSet spec: template: spec: ignoreDifferences: - group: gateway.networking.k8s.io kind: HTTPRoute jqPathExpressions: - .spec.parentRefs[].group - .spec.parentRefs[].kind - .spec.rules[].backendRefs[].group - .spec.rules[].backendRefs[].kind - .spec.rules[].backendRefs[].weight - .spec.rules[].matches syncPolicy: syncOptions: - ServerSideApply=true - RespectIgnoreDifferences=true

6th Aug 2026 • 1 votes
GitOps with ArgoCD

Now we’re getting to the fun stuff: GitOps. The act of pushing beautifully crafted .yaml files and seeing your Kubernetes cluster get red and stall out is surely what life’s all about. Install ArgoCD We have our Kubernetes cluster and now we’re going to bootstrap ArgoCD so it starts syncing from our git repository. Using helm: helm install argocd \ --repo https://argoproj.github.io/argo-helm \ argo-cd \ --namespace argocd \ --create-namespace \ --version 10.1.4 ArgoCD will by default generate an admin password and you can get it with the argocd tool: # You can use the password to login to the web. argocd admin initial-password -n argocd # Make ArgoCD available at localhost:8123 kubectl port-forward -n argocd svc/argocd-server 8123:80 You can use the argocd CLI to manage ArgoCD but I’m going to prefer the declarative way. Fixed IP You can use the port forward method above to access ArgoCD but having a fixed IP is nicer, especially if you want to use the argocd CLI (I only used it when trying to get it all working). I use a separate service for the IP, which allows me to assign an IP outside of the Cilium load balance pool we setup previously (10.1.4.101–10.1.4.255). It looks like this: apiVersion: v1 kind: Service metadata: name: argocd-server-ip namespace: argocd spec: type: ClusterIP externalIPs: - 10.1.4.51 selector: app.kubernetes.io/name: argocd-server ports: - name: http port: 80 targetPort: 8080 It simply routes 10.1.4.51:80 to the IP of argocd-server and port 8080. We need to apply it: kubectl apply -f gitops/bootstrap/argocd.yaml And you should see it assigned and able to visit it on the web: $ kubectl get svc -n argocd argocd-server-ip NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE argocd-server-ip ClusterIP 10.105.209.116 10.1.4.51 80/TCP 3d18h Repository I use a self-hosted Forgejo instance (hosted in a Proxmox LXC) but you can use whatever you want. You need an API token with read permissions. ArgoCD documents a declarative setup that we’ll use. For repositories we need to create a secret: kubectl create secret generic home-ops-repo \ --namespace argocd \ --from-literal=type=git \ --from-literal=url=<repo-url> \ --from-literal=username=<username> \ --from-literal=password=<token> \ --dry-run=client -o yaml \ | kubeseal --cert infrastructure/sealed-secrets-cert.pem -o yaml \ > gitops/bootstrap/argocd-repo-secret.yaml We also need to add the repository label to the generated file so it looks something like this: apiVersion: bitnami.com/v1alpha1 kind: SealedSecret metadata: name: home-ops-repo namespace: argocd spec: encryptedData: password: ... type: ... url: ... username: ... template: metadata: name: home-ops-repo namespace: argocd labels: argocd.argoproj.io/secret-type: repository Apply it: kubectl apply -f gitops/bootstrap/argocd-repo-secret.yaml And the repo should show up in the UI or argocd repo list. Initial admin password Instead of letting ArgoCD generate a random password we can set the initial password explicitly. It’s possible to do it either via the Helm chart or the declarative way, which I’ll continue with. After generating it with kubeseal the secret should look like this: apiVersion: bitnami.com/v1alpha1 kind: SealedSecret metadata: name: argocd-secret namespace: argocd spec: encryptedData: admin.password: ... admin.passwordMtime: ... server.secretkey: ... template: metadata: labels: app.kubernetes.io/name: argocd-secret app.kubernetes.io/part-of: argocd name: argocd-secret namespace: argocd With three keys: password is a bcrypt hash: argocd account bcrypt --password 'supersecret' passwordMtime is the plaintext modification time: date -u +"%Y-%m-%dT%H:%M:%SZ" secretkey is a random value: openssl rand -base64 48 ArgoCD uses server.secretkey to sign session and API tokens, and it will regenerate the key if restarted. I kept getting logged out when I was rebuilding things all over the place and it was annoying so I added this to make it stable. A file structure ArgoCD is now installed but at this point I think it’s good to take a step back before we steam ahead. One of the benefits of ArgoCD is that you can create any kind of file structure and organize the repository any way you want. That’s also one of its problems: if you can do anything it’s hard to decide on a plan forward. I found the blog post How to Structure Your Argo CD Repositories Using Application Sets illuminating and even though their setup is overkill for my homelab, I simplified their ideas into something that works for me. The basic idea is to have all applications in their own folders under gitops/apps, and place all bootstrap stuff under gitops/bootstrap. Borrowing the “level” terminology from the above post here’s the basic idea: digraph { rankdir=TB "root.yaml" [class="loader"] "appset.yaml" [class="loader"] bootstrap_more [label="…"] apps_more [label="…"] "root.yaml" -> "cilium_config.yaml" "root.yaml" -> "argocd.yaml" "root.yaml" -> "appset.yaml" "root.yaml" -> bootstrap_more "appset.yaml" -> "authentik" "appset.yaml" -> "actualbudget" "appset.yaml" -> "miniflux" "appset.yaml" -> "homeassistant" "appset.yaml" -> apps_more // Keep the "…" boxes on the far right of each level. { rank=same; "cilium_config.yaml" -> "argocd.yaml" -> "appset.yaml" -> bootstrap_more [style=invis] } { rank=same; "authentik" -> "actualbudget" -> "miniflux" -> "homeassistant" -> apps_more [style=invis] } } Which loads in three phases: root.yaml Everything under bootstrap/ gets loaded bootstrap/appset.yaml in turn loads all folders under apps/ This is what the directory structure looks like: gitops ├── root.yaml # Level 1 ├── bootstrap # Level 2 │ ├── cilium_config.yaml │ ├── argocd.yaml │ ├── appset.yaml │ └── ... └── apps # Level 3 ├── authentik │ └── ... ├── miniflux │ └── ... └── ... I’m considering separating infrastructure and user workloads, but for now I’m fine with having everything in the apps/ folder. App-of-apps root.yaml will be a master “app-of-apps” as it will be an Application that spawns other applications. It looks like this: apiVersion: argoproj.io/v1alpha1 kind: Application metadata: name: all-apps namespace: argocd annotations: argocd.argoproj.io/sync-wave: "-100" finalizers: - resources-finalizer.argocd.argoproj.io spec: project: default source: repoURL: https://git.hietala.xyz/tree/home-ops.git targetRevision: master path: gitops/bootstrap destination: server: https://kubernetes.default.svc namespace: argocd syncPolicy: syncOptions: - CreateNamespace=true automated: prune: true selfHeal: true (You’re going to see lots of yaml files like this, better get used to it.) The important bits are The name all-apps in the argocd namespace It should be synced before everyone else (sync-wave: -100) Should load everything under gitops/bootstrap from my home-ops repo I’ll be using pruning and self-healing to tell ArgoCD to keep everything in sync. With selfHeal: true ArgoCD will revert any manual kubectl changes to a managed resource. This is the “proper GitOps” way as you must go through git but in practice it might be annoying. If you turn it off the ArgoCD will track the difference but won’t fight you. This is the entrypoint and it’s the last manual apply command we’ll use, as it’ll load everything else: kubectl apply -f gitops/root.yaml Application sets Inside the bootstrap/ folder lives, among other things, bootstrap/appset.yaml which is responsible for turning subfolders under apps/ into applications and loading them. This is what I use and it’s an adaption of the code from the aforementioned blog post: apiVersion: argoproj.io/v1alpha1 kind: ApplicationSet metadata: name: appset namespace: argocd spec: # Use Go's engine so we can use basename/path goTemplate: true # The engine silently fails on missing keys by default goTemplateOptions: ["missingkey=error"] # Generate from all subfolders from my repo under apps/ generators: - git: repoURL: https://git.hietala.xyz/tree/home-ops.git revision: master directories: - path: gitops/apps/* template: # The Application name will be folder + "-app" # Creative, I know. metadata: name: "{{.path.basename}}-app" spec: project: default # Describes the manifests of the application, # which is everything under apps/<app-folder>/ source: repoURL: https://git.hietala.xyz/tree/home-ops.git targetRevision: master path: "{{.path.path}}" # Give the application its own namespace destination: server: https://kubernetes.default.svc namespace: "{{.path.basename}}" syncPolicy: syncOptions: - CreateNamespace=true # Needed for larger manifests - ServerSideApply=true # ArgoCD does a dry run to check for resource kinds # it doesn't yet recognize. This breaks some things # so turn it off. - SkipDryRunOnMissingResource=true automated: prune: true selfHeal: true I hope the comments make it clear how the above works. The end result is that everything under apps/ will be loaded and organized under its own application. Other bootstrap things Manifests under bootstrap/ Even though we manually apply some manifests during the bootstrap sequence, they can still be managed by ArgoCD after the fact. This is solved by simply placing the relevant files in bootstrap/, and they’ll be managed going forward. These are what we’ve collected so far: Cilium We installed Cilium using a helm install command. Here’s a (hopefully) corresponding example of a manifest that ArgoCD can manage: apiVersion: argoproj.io/v1alpha1 kind: Application metadata: name: cilium namespace: argocd annotations: argocd.argoproj.io/sync-wave: "-1" finalizers: - resources-finalizer.argocd.argoproj.io spec: project: default source: repoURL: https://helm.cilium.io chart: cilium targetRevision: 1.19.2 helm: values: | kubeProxyReplacement: true k8sServiceHost: 10.1.4.10 k8sServicePort: 6443 l2announcements: enabled: true externalIPs: enabled: true gatewayAPI: enabled: true ipam: mode: kubernetes operator: replicas: 1 securityContext: privileged: true destination: server: https://kubernetes.default.svc namespace: kube-system syncPolicy: syncOptions: - CreateNamespace=false - ServerSideApply=true automated: prune: true selfHeal: true The duplication here is unfortunate but I couldn’t find a way around it. At least we can reuse cilium_config.yaml with the load balancer IP specifications that we created during cluster setup. Just plop it under bootstrap/ and we can move on. Gateway API Before installing Cilium we also applied the Gateway CRDs using this command: kubectl apply -f https://github.com/kubernetes-sigs/gateway-api/releases/download/v1.2.1/standard-install.yaml This is the corresponding YAML manifest (smaller as we don’t pass dozens of options): apiVersion: argoproj.io/v1alpha1 kind: Application metadata: name: gateway-api namespace: argocd annotations: argocd.argoproj.io/sync-wave: "-2" finalizers: - resources-finalizer.argocd.argoproj.io spec: project: default source: repoURL: https://github.com/kubernetes-sigs/gateway-api.git targetRevision: v1.2.1 path: config/crd/standard destination: server: https://kubernetes.default.svc namespace: kube-system syncPolicy: automated: prune: true selfHeal: true Note that I specify sync-wave -2 for the Gateway and -1 for Cilium because the Gateway should sync before Cilium. Sealed Secrets We continue in the same manner of converting Helm installations to manifests: apiVersion: argoproj.io/v1alpha1 kind: Application metadata: name: sealed-secrets namespace: argocd annotations: argocd.argoproj.io/sync-wave: "-1" finalizers: - resources-finalizer.argocd.argoproj.io spec: project: default source: repoURL: https://bitnami-labs.github.io/sealed-secrets chart: sealed-secrets targetRevision: 2.16.2 helm: values: | fullnameOverride: sealed-secrets-controller destination: server: https://kubernetes.default.svc namespace: sealed-secrets syncPolicy: syncOptions: - CreateNamespace=true automated: prune: true selfHeal: true ArgoCD In addition to the secrets used for the declarative setup (repository, admin password) ArgoCD can also manage itself: apiVersion: argoproj.io/v1alpha1 kind: Application metadata: name: argocd namespace: argocd finalizers: - resources-finalizer.argocd.argoproj.io spec: project: default source: repoURL: https://argoproj.github.io/argo-helm chart: argo-cd targetRevision: 10.1.4 helm: values: | configs: secret: createSecret: false destination: server: https://kubernetes.default.svc namespace: argocd syncPolicy: syncOptions: - CreateNamespace=false automated: prune: true selfHeal: true --- # Static IP as shown previously, `---` separates different kinds Updated bootstrap sequence This is the updated bootstrap recipe that I manage with Just: [doc("Bootstrap everything from zero")] full: just bootstrap::cluster just bootstrap::cilium just secrets::restore_sealed_secrets_private_key just bootstrap::sealed_secrets just bootstrap::argocd [doc("Bootstrap ArgoCD")] [working-directory('../gitops')] argocd: helm install argocd \ --repo https://argoproj.github.io/argo-helm \ argo-cd \ --namespace argocd \ --create-namespace \ --version 10.1.4 \ --set configs.secret.createSecret=false # We don't let ArgoCD generate the secret, we supply one: kubectl apply -f bootstrap/argocd-secret.yaml # Needed to be able to sync from repo. kubectl apply -f bootstrap/argocd-repo-secret.yaml # Wait for ArgoCD to deploy. kubectl rollout status deployment/argocd-server -n argocd --timeout=300s # Technically not needed as it'll get loaded on sync, this just speeds it up a little. kubectl apply -f bootstrap/argocd.yaml --force-conflicts --server-side # Sync everything else from the repo. kubectl apply -f root.yaml Renovate A pull request generated by the Renovate bot (upping its own dependency). To complete our GitOps setup and to exercise our newfound capabilities let’s setup the Renovate service. Its purpose is to monitor git repositories and to create pull requests where it updates dependencies, see the above image for how it’s created a PR to update itself. The singularity is here any second now. To get Renovate setup we need to create three things: The secret Renovate will pick up RENOVATE_TOKEN in secrets, generated like so: kubectl create secret generic renovate-token -n renovate \ --from-literal=RENOVATE_TOKEN='<token>' \ --dry-run=client -o yaml \ | kubeseal --cert infrastructure/sealed-secrets-cert.pem -o yaml \ > gitops/apps/renovate/renovate-secret.yaml We’ll later add the name renovate-token to the main manifest. The configuration file Renovate has a bunch of configurations you can add. I’m good with the defaults except to modify the ArgoCD settings to support the custom directory structure I’m using: { "$schema": "https://docs.renovatebot.com/renovate-schema.json", "extends": ["config:recommended"], "argocd": { "managerFilePatterns": ["/gitops/(apps|bootstrap)/.+\\.yaml$/"] }, "kubernetes": { "managerFilePatterns": ["/gitops/(apps|bootstrap)/.+\\.yaml$/"] } } This activates the argocd and kubernetes managers, where argocd watches ArgoCD specific manifests (Application/ApplicationSet) and kubernetes watches plain manifests (such as container images in Deployment). The manifest The Application manifest is similar to the ones we’ve seen before: apiVersion: argoproj.io/v1alpha1 kind: Application metadata: name: renovate namespace: argocd finalizers: - resources-finalizer.argocd.argoproj.io spec: project: default source: repoURL: https://docs.renovatebot.com/helm-charts chart: renovate targetRevision: 46.236.3 helm: values: | existingSecret: renovate-token cronjob: schedule: "0 1 * * *" renovate: config: | { "platform": "forgejo", "endpoint": "https://git.hietala.xyz", "repositories": ["tree/home-ops"], "gitAuthor": "Renovate Bot <[email protected]>", "automerge": false } destination: server: https://kubernetes.default.svc namespace: renovate syncPolicy: syncOptions: - CreateNamespace=true automated: prune: true selfHeal: true Note that we configure the secret (renovate-token), how often it should run, and the repository information. Commit and push and it should be ready to go. ArgoCD has a really good UI where you can see what’s happening, if you made a mistake somewhere, and also trigger the cron job manually. Or you can use kubectl, for example: # See the health status of all applications kubectl get applications -A # Trigger the cron job kubectl create job -n renovate --from=cronjob/renovate renovate-manual # Watch the logs kubectl logs -n renovate -l job-name=renovate-manual -f With this we’ve got our first example of managing a Kubernetes service via ArgoCD. We’ll continue to see more examples of this as the series continues.

23rd Jul 2026 • 1 votes
Designing a personal Pebble watchface

I recently got a Pebble Time 2 as it seemed like a fun smartwatch away from Google/Apple/Samsung with a good 4 weeks of battery life. One thing I wanted to do is to create a custom watchface for my specific problems. It took more effort to design the watch than I had anticipated and there’s a deceptively large amount of thought that has gone into some of the features here, so I thought it’d be interesting to write a little about it. I don’t get 30 days of battery life with the watch; in practice it’s closer to ~2 weeks, with heart rate monitoring disabled. Issues I want the watch to help me with It’s difficult to describe what I’ve been going through the last ~6 months. It started with a normal depression (yeah, a “normal depression” says a lot already) but it was followed by what felt like a big increase in volume; manageable issues I’ve had forever were suddenly overpowering. I’ve always found it difficult to start chores and boring tasks, yet now it felt like trying to swim through quicksand. At the same time I had huge problems with hyper focus. I could start working on something and the entire day would just disappear; I would sit in front of the computer for 8 hours without any break, interrupted by having to go get my kids from school and I realize that my bladder is exploding and I hadn’t eaten lunch yet. Then I would spend the rest of the day thinking about it, even having difficulty falling asleep because I’m still thinking about it. Needless to say, my time management has gone out the window, even missing appointments for the first time ever. Of course, a watchface won’t solve any of these but I’m desperate and nothing I’m trying is helping. Concretely I’d like the watch to help me with these things: I was going to name the watchface “ADHD hero” but that was a little on the nose as I don’t have an ADHD diagnosis (yet… The investigation is under way but it’s not a quick process). It’s common for people to claim they have ADHD, like the cool kids. The issues I’m having are quite severe and it’s not something “cool to have”. A watchface only for me I like open source but I don’t have any plans of publishing the watchface. The entire point of the watchface is to remove friction in my life, not create another source of anxiety. This way I’m free to change it, rewrite it, pollute it with vibes, and I can focus on making it just good enough for me without having to worry about others. I encourage you to steal anything you like and create your own watchface; it’s straightforward with Pebble. Prototyping with Claude I’m a big believer in being able to quickly iterate through different prototypes. I’ve had success with using Claude to generate HTML prototypes and it can spit out images like this: Design-wise it’s hit-and-miss and Claude seems to lack creativity and taste (but sometimes it comes up with great ideas). You need to guide it properly and I’ve gotten good usage from telling it to generate dozens of variations of a concept and iterating from there. All images in this post are generated in this way at various points of the development. It can also quickly generate interactive prototypes, like this embedded widget that simulates (almost) the entire watchface and its features: A simulation of the watchface, generated by Claude. This is super useful and I’ve used it to fine tune the wedge geometry, color scheme, a chiptune library, and more. These are rough prototypes and they serve to give a direction, not to be a perfect representation of the end result. The interactive widget above for example has a bunch of rendering artifacts that don’t exist in the real watchface. Finding a style The design of the watchface started before I had the watch and before I wrote a line of code. Initially the feel was very different; it was a cleaner, more uniform, design without as much clutter and everything followed a consistent design language. I just knew that it was perfect. The original watchface. Tracking tasks. When the watch arrived I built it and it sucked. It was so boring… I saw the Comic Drop watchface and its comic book panel layout is really interesting. Wouldn’t it be cool to have a similar comic book panel layout that would show the events during the day? Maybe they could be dynamic and move around during the day? A comic book panel subdivision. It would be really neat to have some pixel graphics of a computer during the morning when I should work, a picture of a barbell when I should exercise, etc. Unfortunately that’s beyond my skill level at the moment so I had to give up that idea. So I went back to the original design but iterated on a “comic book” style with thicker lines, brighter colors, and more irregularities: The new comic book style, with elements I’ll discuss later. This is a more interesting watchface and it looks much better on the wrist than the first iteration. Not perfect of course but it catches my interest, which is the point. Calendar overview I want to be able to glance at the watch to see my calendar. Watchfaces such as the Sectograph add event information inside the clock (so an event between 1 and 2 would colorize the section of the analog clock) and I wanted something similar. Here are some prototypes to give you an idea of what I’m talking about: Single event in the day. Four events the next 6 hours. Fully loaded day. This should hopefully make it easier for me to be able to plan my day a little better. Pebble already has built-in notifications for events that work well. This is purely for me to visualize the day. A notable omission in my design is the lack of a text description of the event. That’s nice but it also clutters the design I’m going for. The important thing for me isn’t what the event is, it’s “is there an event at all”. Non-uniformity My brain is very good at filtering away static and uninteresting information. For example, I’ve tried to have Calendar, Habitica, and Todoist widgets on my phone to help me keep track of my events/habits/tasks… But it didn’t take long for my brain to start completely ignoring them despite them taking up 80% of the screen. When I say ignore I mean that literally: I completely stopped noticing them. So what I wanted to try was to spice it up a little by randomizing the event geometry. See these two examples: Uniform events. Non-uniform events. To me the non-uniform prototype is more visually interesting and the changing geometry should hopefully prevent me from filtering them out. The goal is to give a general overview of the day, not to display a precise calendar. I tried to have each event size correspond to the event length but I abandoned that for more randomization. To make generation simpler (and to save on battery) I snap events into 15-minute intervals. I’m not sure yet if this is a good call but so far it’s fine. You can argue that the rounder uniform version would look better on the Pebble Round 2, which is fair. Here’s a similar mockup but with the round’s geometry: Uniform events on the Round 2. Non-uniform events on the Round 2. I still think the non-uniform variant catches the eye more and solves my specific problem a bit better but there’s no denying that a round layout on a round watch fits very well. Moving towards 12 o’clock Another idea I had was instead of statically fixing events to their times I wanted the events to move towards “now” at 12 o’clock. So an event that renders at 3 o’clock is 3 hours away and an event that overlaps 12 is ongoing. While this prevents me from easily looking back at my day (past events disappear) it makes it easier to feel the urgency as events draw closer. A bit weird perhaps but in practice it feels great. Here’s the same event through its lifecycle: drifting in from 3 o’clock, ongoing as it crosses 12 (with the countdown band draining in the clock), then gone once it’s in the past. Something like this: Event is a few hours away. Event is ongoing. Event is in the past and has disappeared. Completion countdown This works pretty well but there’s a problem: how to differentiate between an event that will start in one minute and that has been going on for a while? Because the event gets cut off at 12 there’s no way to tell at a glance. At first I tried to enlarge the event wedge so it touches the clock if it’s ongoing. That was a bit weird so instead I tried to colorize the clock outline with the event that’s ongoing. Then I realized, why not have the outline count down the remaining time of the event, similar to a Time Timer? The countdown band draining as the event completes. In practice this works out wonderfully well. I had dozens of prototypes for line weights, fill direction, the clock itself, etc but I won’t bore you with them. I tell myself that real design should appear to be effortless. Work timer I quite like the completion countdown for calendar events. This kind of design is often associated with tracking tasks (such as in Pomodoro) so why not add it to the watch too? For example, I might want to configure a stretch of 60 min work / 15 min break / 60 min work before I should stop working. Pretty standard stuff. As I’m struggling to get started I’ll add a twist: a working block should have a 5-minute warm-up period. So the timeline looks like this: warmupwork break warmupwork 60 min 15 min 60 min I chose to include the 5 minute warm-up into the work length, so they’re both 60 minutes long instead of 65 minutes. I think it makes sense to have that one count down (from filled to empty) while the working block counts up (from empty to filled), like so: Task 5-min warm-up followed by two 1 hour rounds, with a 15-min break between. Starting/stopping timers on a Pebble watchface This is a post about design, not about implementation, but I still need to address how we can implement a work timer toggle. The issue is that a Pebble watchface is a background process that cannot interact with Pebble’s physical buttons. You can bind the buttons to quick launch Pebble apps but not watchfaces. A watchface can detect steps and taps on the watch but it cannot communicate with other apps. There’s a few ways to get around this: Toggle using taps only. Implement it as a Pebble app, not a watchface. Add a new Pebble app (tied to a button) that round trips commands via the companion app back to the watchface. digraph { rankdir=TB button [label="Quick launch\nbutton"] app [label="Pebble app"] phone [label="Companion app\non phone" class="external"] face [label="Watchface"] button -> app [label="press"] app -> phone [label="toggle"] phone -> face [label="toggle"] { rank=same; app; face } app -> face [style=invis] } I first tried to implement toggling using taps but it was too unreliable. If I went the app route I’d have to relaunch it all the time, which represents unacceptable friction. What’s left is the janky phone round trip solution. I already need a companion app for the calendar sync but it still makes me queasy. Crap like this is why I don’t feel like publishing the watch. *shudder* Clearing alarms and forcing breaks I’ve tried alarms, Pomodoro, and similar before but they all share the same failure mode: I simply ignore the alarm and continue with what I was doing. A Pomodoro can’t be interrupted; it marks 25 minutes of pure work. Francesco Cirillo, The Pomodoro Technique I don’t know, it could be 2 minutes just as likely as 200 minutes. So I figured I’ll try to force myself to take breaks by making it annoying to clear (a bit like the Hand Grenade Alarm clock but in wrist form). The watchface can vibrate, play sound, react to steps, and detect taps and this is the flow I came up with: The alarm transforms the screen and vibrates and plays a tune until it’s dismissed (repeats every 30 seconds). I had to silence it during calendar events to not mess up meetings too much. Visually the required steps are drawn at the border (no events are shown during an alarm) and the alarm is drawn as a comic-style blast (tapping it shrinks the blast). Like this: Dismissing the end of day alarm. How well does it actually work? It’s a pretty cool setup and it is helping but it still fails quite often. Most of the time I forget to start the work timer and other times I’m able to add enough steps by waving my hand around like a crazy person, and then continue working. (If I raise the step count then it gets annoying to dismiss when I’m actually walking.) Could be tuned better I suppose. Other types of alarms Time for a break Stop working End of day Manual alarm Visually there are some distinct alarms I use: I’ve separated them visually and they each use unique vibrations and tunes. An implementation detail: Pebble doesn’t natively render text at an angle. I worked around it by rendering the text at an angle to a bitmap, and drawing that. Other information I’ve previously used watchfaces overloaded with all kinds of information and graphs that were cool for a few minutes and then I never looked at them again. But there are some things I regularly glance at such as the date or if the watch has lost its phone connection. Date and week number Here are some prototypes for dates that I liked but ultimately discarded: The difficulty is to not crowd the watchface too much with the date and the other elements I’ll get to later. In the end I ended up with a simple 31/10 label and a small weekly text without background as I don’t look at the week number that often: The date label together with the weekly number floating freely. I’m not completely sold on the week number display (yellow labels were a lot more fun) but it’s good enough I guess. Step counter It was really difficult for me to find a step counter design I liked. Everything I tried just felt off. Here are three of my attempts: Steps are tracked at the edges. A health bar of some sort. Steps in a separate column. I used to track steps at the edges for most of the design (you can see the step elements in many of the prototype images in this post) but in the end it introduced a bit too much clutter for my taste. While the other two attempts above are crude and could be polished more I didn’t see potential in them. (Examining and cutting away branches is important during design I think.) And then the revelation: I don’t care about the exact step count. The only purpose of showing steps is to help me keep the step count up, so I’m not sitting in front of the computer all day, and having badges for tiers solves that problem even better. Here are the badges I use: Bronze Silver Gold Diamond Bronze is achieved at 33% of my daily goal, silver at 66%, gold at 100%, and diamond is for over-achieving with 150%. I feel that having an easy to reach tier helps me do something on the lazy days and the hard-to-reach tier helps give me an extra kick the days I’m out and about. Yes, not knowing exactly how much is left to the next tier is slightly annoying, but for those cases opening up the Health app on the watch to view the step count is good enough. Battery states High after the rush of figuring out how to display steps let’s use the same approach for battery warnings: Ok, above 15% Low, below 15% Critical, under 5% Yes, I probably should make the little battery interactive or something but this is a 80:20 situation: I get 80% of the benefit from 20% of the effort. It’s good enough. Bluetooth & Sleep / DND More badges! Bluetooth disconnected. Phone in DND or Pebble in Quiet Time. Pebble has a “Quiet Time” mode that silences notifications. This is a separate thing from Android’s sleep mode or do-not-disturb mode. As I have a companion app I can detect the Android state and the watchface combines the two (and silences task alarms for instance) while also showing a “sleep” bubble so I can see these invisible states. The next task I’m a big fan of some of the ideas of Getting Things Done such as the idea of the “next task”: the one actionable thing that you should do next. I’ve used the Todoist Android widget on my phone to display my tasks but I wanted to incorporate it into my watch too. The idea is to only display the next task, something like this: While I think all three prototypes above are visually pleasing I went with the wider display that takes up less vertical space: The next task displayed at the bottom. As a bonus I don’t have to mess around with rendering angled text. Completing/rescheduling the task It works fairly well but there’s a wrinkle: when I’ve done the task or realize I can’t do it now I have to pull up my phone and that’s annoying so in practice I’m leaving a stale task alone forever, priming my brain to ignore it. I’ve tried the Todoist Mini app on the Pebble but there’s so many clicks to find the task and complete or reschedule it. What I did was add it to the app I’m already using to toggle the work timer and give it a menu of options: Completing the next task via the watch. I can still toggle the work timer, complete the next task, or reschedule the task (it’s rescheduled for tomorrow). I’ve bound it to the UP button via “quick launch”. To keep toggling the work timer as simple as possible pressing UP again toggles the timer (so double clicking UP when the watchface is visible toggles the timer). Otherwise it uses the standard Pebble control scheme (UP/DOWN to select and SEL to execute) and the app closes and returns to the watch when an item is selected. So far it’s been working really well but as has always been the case before with these productivity “fixes”; I always tire of them eventually. There’s some companion app interactions I gloss over here (such as latency and if the phone isn’t reachable) but it’s fine, don’t worry about it. I did add a vibration + chime confirmation on the watch as a confirmation that the round trip was completed and it gives a small dopamine hit. Edge markers As a little extra I added some markers to track other types of things along the edges: Markers moving on the edge for alarms, calendar reminders, end of day, and sunrise/sunset. The colored dots for calendar events are genuinely useful as many events require some preparation/travel time. In these cases I add a reminder for the event at, say, 15 minutes before. With the dots this preparation time can now also be seen by glancing at the watch (Pebble natively shows reminder alerts anyway.) The end of day marker is also useful for when I’m planning my working day but sunrise / sunset is mostly a gimmick, and I don’t use the manual alarms . The end result When it’s clear. When it’s (very) busy. At the end I got an interesting watchface I think looks fun and that helps me go about my day. It’s pretty easy to make one, and if you don’t know where to begin LLMs can shorten the time to get up and running.

26th Jun 2026 • 1 votes
Planning my Kubernetes homelab

The Kubernetes iceberg. If I’d have to describe my homelab setup via analogy I guess it would be similar to me on a unicycle carrying plates with both of my hands, or maybe a leaking barrel with water that I try to patch up with silver tape. I’ve also been Kubernetes-curious so I decided to completely redesign my homelab, centered around Kubernetes. It was a bit painful but at least it fulfilled my need for procrastination very well. Overarching goals I’ve got three goals with the setup: Declarative, reproducible, and automated The big goal is to have everything declarative in a single git repository and to easily be able to bootstrap from nothing to a fully working setup. I want to use Infrastructure as Code to create the Kubernetes cluster and GitOps to populate it with all my services automatically from the repo. It should be really easy to make a change; I want to move away from having to ssh into the correct repo and manually do stuff. Backups, backups, backups While a proper GitOps setup means that infrastructure and configuration files are inherently backed up, a proper backup setup is still crucial. Ask me how I know. I haven’t had a proper (as in working) backup solution for years and this time I should have it from the start. Documentation What if I could document my setup, so future me has a chance to understand what’s happening? Writing documentation is boring, so I’ll write some blog posts instead. I’m a bit skeptical that I can fulfill all three goals, but if I manage 2/3 or even 1/3 it’s still a big win compared to my old setup. Kubernetes, too complicated? It’s a fair question and the most common critique towards Kubernetes is that’s just too complicated (especially for a homelab). Discussions online are filled with comments such as: Kubernetes has to be most complex software I’ve ever tried to learn. I eventually gave up and decided to stick with simple single machine docker-compose deployments. trinovantes “Let’s use Kubernetes!” Itamar Turner-Trauring So why would I choose Kubernetes? Because, for whatever reason, Kubernetes is very popular and for every comment complaining about complexity you have comments extolling it’s virtues: I was skeptical about Kubernetes but I now understand why it’s popular. The alternatives are all based on kludgy shell/Python scripts or proprietary cloud products. zelly Kubernetes is the biggest quality-of-life improvement I’ve experienced in my career anchochilis Having experienced the single machine docker-compose deployments, kludgy shell scripts, and proprietary cloud products; I think I need to use Kubernetes myself to be able to form an opinion on it. And in some ways, isn’t experimentation a core part homelabbing? Tech stack There are many valid tech choices for this kind of setup and many of them are reasonable. I don’t know if my choices are reasonable—most were chosen because they sounded cool, others because I just picked one. Here’s list of some of the choices I made, which we’ll setup in this series: Talos Linux for Kubernetes nodes. The coolest way to run Kubernetes. Lightweight and secure, what’s not to like? Terraform to provision VMs on Proxmox and to initialize Talos Linux. Cilium for proxying, CNI, load balancer, and Gateway API provider. I opted for Cilium as it’s one dependency replacing several alternatives (such as kube-proxy, Metallb, and Traefik, which I was leaning towards at first). Gateway API is the new thing you “should” use instead of ingress, and I wanted to try it out. ArgoCD for GitOps. If it was purely for myself FluxCD might have been the better, simpler, choice but we might use ArgoCD at work and I don’t want to deal with two separate systems at the moment. Renovate to keep dependencies up-to-date. CloudNativePG for Postgres on Kubernetes. I’ll also setup timescaledb, although we won’t use it in this series. It’s just to prepare for the future migration of long-term statistics from Home Assistant. Longhorn on NVMEs for persistent storage. Data is backed up using VolSync and Restic. Sanoid, Syncoid and Kopia for backup archive management. Backups are snapshotted and stored in ZFS, which are also encrypted and shipped off-site to Backblaze for storage in the cloud. Backups from Longhorn and Postgres arrives to ZFS via Garage, a self-hosted S3 service. Authentik as an identity provider and single-sign-on platform. It’s nice to not have to login manually everywhere. Huh. Displayed like this it looks like a lot, but fear not! It’ll be worth it in the end. In the next part we’ll start by creating VMs and getting a Kubernetes cluster up and running.

5th May 2026 • 1 votes

More in technology

Refurb weekend II: Alpha Micro Eagle 300 and friends

This Refurb Weekend is a sequel in two ways. Six years ago I fixed the power supply and case fan in my 68EC030-based Alpha Microsystems Eagle 300, a more modernized and substantially faster Alpha Micro running AMOS, their unique DEC-inspired multiuser operating system. These blue-collar boxes were very common in many vertical markets due to their tightly integrated business-specific software packages, and are especially unusual for running their big-endian 68000-series processors little-endian (for data file backwards compatibility with their WD-16 ancestors). The 1990s-era Alpha Micro Eagle family improves on the originals with later 68K and even ColdFire CPUs, plus Ethernet and faster SCSI. The second way it's a sequel is to our previous Alpha Micro refurb weekend, a double header restoring an Alpha Micro AM-1000 and AM-1200, two members of Alpha Micro's most popular series of computers and some of the closest things they made in the early '80s to desktop workstations. From the outside, the Eagle comes in a PC-style case, but it's not a PC: ampm.floodgap.com using the webserver provided with AlphaTCP — yes, Alpha Micros can be networked — and its own on-board Ethernet. At the time the cooling fan failure was putting a lot of strain on the hard disk because it was overheating, and since this was my first Alpha Micro and I wanted to keep it in good nick, I decided I would put a SCSI2SD in it after the repair and get rid of the spinning disk entirely. Fast-forward to last fall, a little over a year ago, when I tried to do exactly that with a ZuluSCSI. And then it wouldn't start. thought was a replacement unit I could raid for parts, but it turned out to be something rather more special — and then I was even more fortunate when I got the Eagle 300 back out of storage to make one last attempt at reviving it. To cap it off we'll go stick ZuluSCSIs in everybody else, including my Motorola ColdFire-based Eagle 450, the first ColdFire server ever produced and what used to be the fastest Eagle in the house until the new hotness came. I even converted the external BlueSCSI the AM-1200 was using to an internal ZuluSCSI too. Now nearly everybody's got a solid-state boot disk and they're all a good bit more zippy, so we'll gawk at the internals, compare 68K CPU and storage benchmarks and most importantly learn from my mistakes, because as my late father always said, "I'd rather be lucky than good." On a more solemn note, we'll also observe a moment of silence for one of the line's creators who passed away last year. It's time for a Refurb Multi-Weekend Sequel. Alpha Microsystems, founded in Orange County, California in 1977, produced what were considered some of the earliest "poor man's minicomputers," smaller and more affordable multi-user systems that run a bespoke, Digital Equipment Corporation-influenced operating system named AMOS. In our previous entry we discussed their history in detail. First-generation Alpha Micros like the AM-100, designed by Dick Wilcox who also wrote most of the operating system, used large S-100 cases and a CPU chipset called the WD-16, derived from the Western Digital MCP-1600 chipset — the same one used in DEC's LSI-11 CPU — but with higher performance microcode developed by Rich Notari. These machines thoroughly outclassed low-end PDP-11s yet were substantially cheaper, and Alpha Micro found a ready audience initially with hobbyists and then later corporate turnkey environments, where its integrated AlphaBASIC and database functionality gave it a competitive advantage. As the WD-16 had little elbow room to evolve, in 1982 Alpha Micro introduced new systems based on the Motorola 68000, porting AlphaBASIC and AMOS to run natively, but swapping address lines to make it appear little-endian for compatibility with WD-16 data files and AlphaBASIC P-code. These new machines included the AM-1000, Alpha Micro's most popular single model, and by 1984 it constituted the majority of the over 20,000 Alpha Micro machines then in active use. An idealized Alpha Micro office from the 1980s might look like this photo, from their actual advertising of the period. The main system is an AM-1042E, the S-100 box sitting on the pedestal next to the printer, using an AM-100/L CPU with an 8MHz 68000 (the same as the AM-1000, which was integrated into a desktop case), and in the factory-standard configuration shipped with 512K of RAM and a 60MB Winchester ST-506 hard disk. The system served some number of local users connected directly via serial ports, in this case the two administrative assistants with AM-60 terminals (a modified and rebadged Wyse WY-100). The fact everyone here was in the same room with the machine was helpful, because AMOS is a strictly real-memory operating system and executable code ran from where it was loaded in the user's fixed memory allocation — there was no memory protection. Although the admin assistants' explicit permissions were limited by which account they were logged into, using the same style of project-programmer numbers as DEC (e.g., [1,2] being the operator account), an errant job of theirs could readily stomp on another and it was not rare for busy systems with buggy software to regularly crash. On the other hand, this also made it fast and efficient with little separation between you and the hardware, and even Alpha Micros with fairly modest specifications could support hundreds of simultaneous users. Alpha Microsystems tried multiple times to broaden the line technologically, though these efforts were largely for naught. Early attempts to put equivalent hardware onto a PC ISA co-processor card, with software to interchange between MS-DOS and AMOS, yielded only an expensive and poorly-selling PC that wasn't good at either OS (eventually the company gave up producing turnkey systems and just sold the card). In 1984 Alpha Micro introduced UNIMOS, a licensed version of AT&T System V UNIX which ran on modified AM-1000 hardware with an MMU, but it was likewise too expensive and garnered little interest from new users nor demand from existing ones. The cost of its development, the Unix license and its associated royalties, and a simultaneous lawsuit from DEC over AMOS's similarities subsequently caused lasting damage to the company, and by the end of the decade Alpha Micro found itself almost entirely dependent on legacy AMOS users. In the wake of Wilcox's 1990 retirement and original investor Bob Hitchcock's own departure in 1991, new management tried once more with Unix systems based on the Motorola 88000 — which Motorola abandoned shortly afterwards for the PowerPC AIM alliance, and so no hardware ever shipped. anybody — yes, really, just two months after the '030's October introduction — initially offering a 20MHz CPU, 32K of L2 cache and supporting up to 360 simultaneous terminals, but even the lowliest 30-user configuration started north of $40,000 [about $117,600 in 2026 dollars]. By contrast, Alpha Micro was uncharacteristically tardy in adopting the 68040 and didn't release the 33MHz AM-4000 until 1993. It was nevertheless nearly as expensive, and for most small offices, which were now increasingly Alpha Micro's financial anchor, that kind of money was simply beyond consideration. In 1993 the company settled on a two-board approach to solve both problems at once. The CPU board, dubbed the Roadrunner and designed by engineer Joshua Stumpf, contained the new processor, standard RAM SIMM slots, L2 cache (if present), and SCSI controller. This board was specifically designed to also fit in legacy machines, specifically the desktop AM-1000, AM-1200 and AM-1600 and server AM-2000 and AM-3000 systems, and initially came in a 68030 and two 68040 versions. A second board served as the CPU board's system interface, linked together using a custom "X-BUS" interconnect over twin 34-pin cables. For legacy installs, this second board was an active interposer physically replacing the original processor, which in elderly units sometimes needed to be desoldered (though that was also true for the few third-party CPU upgrades then extant, like InterLink's Dimension-030, so the Roadrunner was at least no worse). But this second board could be a whole new system architecture of its own, and that was what the I/O board was, designed by Rod Hewitt. It carried the power supply connectors, controlled the system front panel, contained the System Security Device for licensing (more on this later), presented built-in external ports for parallel, SCSI and (for certain loadouts) Ethernet, had an internal port for an optional floppy drive, and provided SIMM-format "paddle card" slots for option cards. Best of all, these boards were smaller and — by using off-the-shelf commodity hardware — cheaper to manufacture, and could be paired into multiple configurations, together becoming the company's long-promised updated low-end and mid-range systems. reasons I've related elsewhere, and I managed to land it for just a couple hundred bucks in fully working order. (Boy, are those days gone.) Based on the software still on the hard disk, this machine was previously used at a chain of three party stores in Washington state as a centralised rental and point-of-sale system, a common turnkey application. On arrival it sported the same loadout as in this article's first picture except for its original smaller-capacity Tandberg QIC tape drive, which in Alpha Micro parlance is called a tape streamer. Since these systems don't boot from CD, QIC tape is your only way to cold start and restore most 68K Alpha Micro computers if the hard disk fails. These systems also don't support the old Alpha Micro VHS backup system, hopefully to be the subject of a future article, which was ubiquitous in their early hardware. There is a single hard disk in the second 5.25" bay, though these first-generation systems also supported 5.25" and 3.5" floppy drives using an AM-219 controller board. original Refurb Weekend. our previous Alpha Micro foray we discussed the SSD's role, a chip installed in every 68K (and later x86) Alpha Micro, even the earliest S-100 boards, used by the operating system and any installed VAR software packages to enforce copy protection and license caps. The SSD number on the rear is just a serial number, printed on the rear plate for convenience and engraved on the SSD chip inside, which I'll show you in a moment. This number is not the key stored inside the chip, up to 256 bytes in length, which only Alpha Micro recorded. When a VAR wanted to lock an install to a particular system, they would provide their private code to Alpha Micro plus the user's SSD serial number, and Alpha Micro would look up the key and generate an alphanumeric PIC (Product Installation Code) that the VAR either used for direct installation or to provide to the user. During the installation process the PIC (usually irreversibly) encoded a binary overlay for the protected executable(s), making it impossible to run them on a system where the SSD didn't match. To facilitate logic board swaps and upgrades, the SSD is socketed and easily transplanted into a new board, but PICs were expensive then and unobtanium now, another reason hard disk backups were so critical. % curl -I http://ampm.floodgap.com/ HTTP/1.0 200 OK Date: Sun, 11 Oct 2026 23:43:03 GMT Server: AMOS-HTTPD/1.3C(111) MIME-version: 1.0 Content-type: text/html Last-modified: Sun, 11 Oct 2026 23:42:00 GMT Content-length: 30242 Content-encoding: 7bit in our most recent article. In this picture it's also attached to an AM-401 CD-ROM, which is the trusty Toshiba XM-5401B in an external case, though only specific ROM versions are supported. It can't boot from this drive, but it can read AMOS-formatted partitions, and with external software can also access ISO 9660 filesystems. Onscreen is a performance estimate using the built-in SI.LIT tool, which we'll be using to benchmark our new solid-state disks when we get them in there. Here's that output. .si dsk0: SI--AMOS System Information, (C) Copyright 1990, Alpha Microsystems Inc. Version 3.0(114) Computer Type: Eagle Main Processor: 68030 Co-Processor: None MMU present: Yes Operating System: AMOS 2.3A(484) Boot PROM revision: M00 Number of SSDs: 1 Total Memory: 16384 K-bytes Resident Monitor Memory: 6218 K-bytes Computing Index (CX), relative to AM-100: 36.9 48 bit FP Index (4X), relative to AM-100: 36.3 Floating Point Index (FX), relative to AM-100/L: 5.6 Disk Index (DX), relative to ST-506: 15.3 I don't know why it identified the 68EC030 as having an MMU, because it doesn't, but we're mostly interested in the "computing index" figure (SI's equivalent of an integer score) which is 36.9 times faster than Alpha Micro's original AM-100, and the "disk index" hard disk throughput test, which rates the spinning disk it came with as 15.3 times faster than their early ST-506 drives. Remember these figures as a baseline; we'll be referring to them again. On the front panel in both pictures you can see the number "0" in the LEDs. That's where the system status is displayed, initially by the firmware, then the OS monitor during boot and then in operation. We'll come back to this as well. original Refurb Weekend involved replacing the old AT supply with an ATX one and a converter. Since that post I found a NOS AT supply for cheap and I needed the ATX supply for something else, so I removed the converter and installed the new AT supply. This is that unit. The CPU and the 53C710 are both clocked by an 80MHz crystal divided down to 40MHz; there are also 16MHz and 50MHz oscillators on the board which appear to be used for the PLD and bus logic. Likely for cost reasons there is no L2 cache, though another thing to keep in mind is — so I'm told — that for compatibility reasons with 68000 and 68010-era AMOS software it is not possible to enable the data cache (or at least not without substantial difficulty). In fact, we observed exactly this phenomenon when looking at the driver for the InterLink Dimension-030 CPU upgrade, which puts a 68030 in an AM-1000: it never set the data cache bit in the '030's CACR at all. Although some machines can accept an external L2 cache, including one we'll look at in detail, anything it would provide is strictly I-cache. but is functionally equivalent in AMOS. not). I have such an example later on. Other serial paddlecards can also be installed. The AM-314 provides only four serial ports, but is not merely a cut-down AM-318 — the AM-314 can also be configured for RS-422 (the AM-318 is strictly RS-232) and supports all RS-232 handshake lines (the AM-318 only RTS/CTS), and in certain configurations can trigger a remote reboot from a connected distant console by grounding pin 1 with pin 7. By contrast, the more exotic AM-338 provides an IBM Binary Synchronous Communication "Bisync" link, typically used for Remote Job Entry (RJE) or CICS connections with IBM mainframes, but could theoretically have also worked with some early point-to-point networks like BITNET. It connects to an AM-339 which provides the physical port. a future article. Notice how many of these PCBs say "Made in USA"! A different era. from my SGI Indigo^2 retrofit to see if it could sit on them. It did, so I sheathed them in electrical tape as a "bed," put the ZuluSCSI into initiator mode, connected the old hard disk and the ZuluSCSI to my copy rig, and left it to image the drive to the SD card. didn't change: the boot sequence wasn't starting. I freaked out, immediately powered it off, and checked everything inside that I'd touched to find I had not properly reconnected the CPU board's power connector. original. I dug up Rod's old E-mail to see if there were any clues in it and recalled what he'd said about the PLDs being dynamically reprogrammed on every startup — from the CPU board. I also knew from the service manual that the X-BUS cables, the only connection between the I/O board and the CPU board, had to be correctly connected or the service manual warned damage could result. A horrible theory stirred in my head: perhaps I had managed to accidentally corrupt the PLDs by effectively bringing up the I/O board without the CPU board, where the boot PROMs used to reprogram them are located. However, that also suggested a solution — there are many undocumented jumpers on the AM-319 and I wondered if one of them could be used to trigger a clean reset of the programmable logic. in a future article). He also had an interest in digital television, developing TSReader, a way to analyse the MPEG-2 transport streams commonly used for satellite, cable and IPTV transmissions. ZTERM and in particular TSReader became far and away his most popular software packages. However, a few years prior and unbeknownst to many, he had quietly entrusted a copy of the TSReader source code to a friend officially "just in case," and sold ZTERM to MicroSabio, a long-time Alpha Micro support company that maintains A-Shell, a way of running AlphaBASIC applications on modern operating systems. The reason became clear later — he was dying, and it was not treatable. The source code for TSReader was duly posted after his passing and remains today as his most popular legacy. Rest in peace, Rod, and thank you. Without Rod's guidance, though, there wasn't much more I could think of to do at that point. I unplugged it, pulled the clock battery and let it sit for a couple days to see if that was enough to reset the PLDs, assuming that was the actual problem, and it didn't. It still seemed like it should be repairable if I could only figure out how, but as there was little point in having it sit fruitlessly on the workbench I removed all the remaining cables from the system board and moved the machine to cold storage until I could come up with something else. In the meantime I turned to my alternative system to get ampm.floodgap.com back up and running, though to discuss it we need to bring the history forward some more. only in the E500: a junior-varsity 68040, which is what the E400 was, didn't have much appeal. Both the E200 and E400 were quietly dropped. For large installs that required substantial numbers of serial ports, the existing AM-319 board had to be expanded. The revised AM-319-20 board, providing four built-in serial and parallel ports with a UPS monitor port and a higher-performance DP83902 Ethernet NIC, still had two paddlecard slots, but these slots were widened to 60 pins and could now be part of a bus connected to an external chassis. Alpha Micro had long sold the AM-990, a large capacity case (in this image as used for the AM-4000, though I apologise I don't have a better image of it) and the AM-990 Jumbo, which was even larger. A new higher-performance serial card was developed to take advantage of the widened paddlecard slots, an optically-isolated eight-port card called the AM-359. Twelve AM-359s could be installed in the AM-990, yielding 96 ports when connected to a fan-out in one of the AM-319-20's paddlecard slots (the four on-board ones made an even hundred); the Jumbo could take another twelve AM-359s for another 96 ports connected to the second, a whopping total of 196 serial ports when combined with the base four. If you needed even more, the AM-3501 sidecar chassis could take you well beyond. There was no point in supporting the 68EC030 with such a monster: this new Eagle, officially the Eagle 550 Super Eagle, was 68040 or bust. that large, a Super Eagle in the standard case could support up to 60 ports, four on the AM-319-20 and then seven AM-359 cards in the usual position. The E550 Super Eagle additionally came as a separate upgrade kit for the E500 and the few E400s that sold, essentially a system board swap, though when sold as a turnkey system it was always with the 40/80MHz CPU. The E550 introduced a new Seiko timekeeper chip as well which not only maintained the time but also the "CMOS settings," the first Alpha Micro with an on-screen setup utility in ROM allowing boot configuration. Subsequently the company was tardy once again in adopting Motorola's newest 68K CPU, this time the superscalar 68060, which had come out in 1994 aimed squarely at the Intel Pentium but didn't reach Alpha Micro customers until mid-1997. This took two forms, a new 66MHz '060 Roadrunner upgrade option for the E550 Super Eagle and the AM-4000 (lesser Eagles need not apply), and a new AM-6000 large system. Interestingly, the AM-6000 is strongly based on the E550, reflected in its internal designation as the AM-3500-6000 making it a "stealth" Eagle. The '060 Roadrunner not only included the new CPU but also 64K of L2 cache, support for up to 256MB of RAM in four SIMM slots, and 68-pin Fast and Wide SCSI-2 using the NCR 53C770. along with E100s and a smattering of E500s and Super Eagles. However, it was not lost on company management that Motorola believed the 68000 family was fast approaching a performance wall. This was made most clear by their then-enthusiastic participation in the PowerPC AIM alliance, but also the rise of ColdFire, which Motorola had first announced in 1994 as well. ColdFire was in its own way Motorola's oblique admission that 68K was losing desktop competitiveness to x86, an attempt to scale the ISA down to embedded markets where its familiarity could be an asset. Approximately 20% of the instruction set such as BCD and certain addressing modes were eliminated, meaning the canonical ColdFire could not be fully binary-compatible with any extant 68K. On the other hand, such cuts enabled the core to execute nearly every remaining instruction in a single clock cycle, and the initial die was a miniscule 4.4mm² at 0.5μm, a third the size of Motorola's own 68300-series CPU32+. Some amount of this work was done by altering the microcode of the 68040V, a fully static 68LC040. Mindful that ColdFire's incompatibility might impair adoption, Motorola chose to create a bridge first-generation chip from this altered 68040V where omitted user mode instructions were left in, demonstrated on die photos. While the new chip had smaller 2K instruction and 1K data caches than the 68040V, and the MMU was also eliminated, this deviation from spec was still sufficient to run regular 68EC040 code at the user level. Motorola called this first-generation ColdFire chip the MCF5102, produced on a 0.6μm process, and introduced it in 1995 drawing 0.8W at 20MHz and reaching 33MHz. Clock-for-clock there was no way this chip could eclipse an '060, and the emerging "true" ColdFire 520x series couldn't run existing Alpha Micro software, but the MCF5102 was still faster than an '030 and nearly as fast as an '040 — in fact, we'll prove it. On top of that it was cheaper and AMOS didn't need the MMU anyway, making for a plausible mid-range machine that wouldn't imperil (much) sales of the E500 or E550. alt.sys.alpha-micro, though credit must be given to John F. G. Leighton, then Alpha Micro's VAR marketing manager, who chose to print the review unedited in marketing materials, even Rod's gentle complaints. Since this is the only E450 I have personally laid my hands on, we will be referring to his comments liberally as a point of comparison, and consequently the E450's history is probably the best documented of any 68K Eagle. It rolled out to users in October at the annual meeting in nearby Riverside, California. This particular system was a gift from someone who inherited it from an 9-1-1 emergency dispatch centre and wanted to find it a good home, another way that running an Alpha Micro-powered web server has paid off. I was able to quickly get into the system (if it's not clear by now, I love hacking into Alpha Micros) and reconfigure it, and it had lurked as the E300's work spare and a test unit since then. Like the E300 it has a bootable tape drive, but this time it's a more modern DDS/DAT. E450, and it claims not to have an SSD at all (untrue). As such, I'm not sure I can believe the stated manufacturing date of September 2002, and at least one date inside directly contradicts it. It's possible this was a case with a transplanted board. 1999 as prerequisites for the AM-370, an unidentified off-the-shelf 100Mbit PCI NIC that goes into the "slot" at J5. To access these PCI slots, that rear panel I pointed out needs to be removed. The AM-370's availability implies all shipping E450s should have had them by then — there was even an alleged board-swap program to do so. I've yet to find another one, so I don't know if that's true, but it would be interesting to find out because with this card or the OEM equivalent you would have two fully operational NICs and multihoming was reportedly supported. I also don't know what's going on with the unpopulated "option connector" at P5 to the right/east of the AT power connectors. other slots. The E450 is one of only a few Alpha Micro systems that can accept an optional external L2 cache, though as mentioned this cache is only enabled for instructions, and the form factor does not match any PC cache stick — in fact, it's the same as a 72-pin SIMM, so don't put the wrong one in the wrong place (Rod also objected to this). The only size supported is 64K, the same cache stick used for the AM-6000 and the '060 Roadrunner, part #PDB-00701-00. I've never seen this cache stick and comparison of his numbers with my cacheless E450 suggests it only yields around a fourteen percent boost, not nothing but not much, likely because it can't do anything for the data cache (though stand by for a very important observation on actual clock speed). The E450 also has two 72-pin SIMM slots, which need not be paired and support up to 256MB, though as with the earlier systems the machine needs to be jumpered to tell it how much is there. The tag on the one in J11 (service order 13009, apparently) is dated January 18, 2002: that makes this machine at least eight months older than its alleged manufacturing date, and to have received an upgrade suggests it's even older than that. Between the two sticks this system has 72MB of RAM. Below/south of the RAM SIMMs are the three paddlecard slots. Also in this image is another obvious non-populated area: a pin-grid array marked "PGA040" at U89 in the northeast/upper right hand corner. That sounds like somewhere the CPU would go, and the footprint looks like an '040. So where's the actual processor? fast is it doing it, though? While Rod's review stated his prerelease system was apparently running at 33MHz, the oscillator closest to the CPU can be seen clearly in this photograph, and it's ... 25MHz. There are no other crystals on the board except for the serial port one you saw earlier, the official marketing announcement has a board layout that also clearly labels this crystal as 25MHz, and nothing in the MCF5102 user's manual says the chip does anything tricky with the clock. As it happens, the only place in Alpha Micro documentation where the E450's CPU speed is given is Rod's review: everywhere else, even the data sheet, simply says it's a ColdFire and nothing more. Since mine is only 14% slower than his, and his had the cache, his can't possibly have been running at 33MHz either. The E450 is a 25MHz ColdFire, full stop. In a backwards way, however, this is nothing less than validation of how performant early ColdFires could be. Recall that our E300, a 40MHz 68EC030, had a "CX" computing index score on the SI.LIT benchmark of 36.9. The same benchmark on this E450 with a 25MHz MCF5102 gets a score of 59.9, and Rod's with 64K L2 came in at a whopping 68 (Alpha Micro said 68.5; either one is around 14% higher). By contrast, the E550 Super Eagle, with a full 40MHz ("80MHz") 68040 Roadrunner clocked 60% faster and 32K of L2 cache, officially gets ... 79.9 (note from the future: my figure is 83.6) — the full cached-up faster-clocked 68040's performance on the same code is only around 33-40% better, and just 16-22% or so if the E450 has its cache too. Put another way, the CX/MHz ratio of the faster 68040 was right around 2.0, while the CX/MHz ratio of the MCF5102 is 2.4 without the cache, and almost 2.75 with it. Likely because the lower clock speed would have looked bad in marketing materials, similar to why General Magic never mentioned the clock speed of the DataRover 840, Alpha Micro simply chose not to mention it either. Still, this machine's surprising efficiency proves ColdFire, at least early on, definitely had legs. I'm not sure how Rod got it wrong except that an earlier version may indeed have run that part, though with these numbers, if there had ever been a production 33MHz E450 it might have seriously endangered sales of Alpha Micro's remaining '040s — perhaps that's why they didn't use it even though the faster part existed. Best guess is that the AM-138 was originally designed for the 68040 and then quickly retrofitted for ColdFire, leaving the PGA region as an "R&D test point" (as documented). our prior entry, and he referred me to a longtime Alpha Micro vendor that they still partner with for existing installations. It took a few months, but this vendor was able to locate what appeared to be another E300 with working boards and we negotiated a price and getting it shipped. "everything console" connected to the top left port (G1) via the dongle at the typical speed of 19200bps. Good, it booted fine, we got a dot prompt, we can get into it, networking is already configured, and (as is typical for most Alpha Micro installations of the era) there is no password on the operator account. I powered it off and now that we know where the system thinks the console is, we'll get a proper self-test readout. I powered it back on with the reset button held in and the self-test came right up. The self-test can autosense the baud rate by mashing the spacebar when the front LED flashes "5b," and then the test runs, with the LED showing the current state and additional details sent to the serial port. ---------------- SYSTEM SELF-TEST ---------------- Version 2.1(120) Prom Rev. N00 for the AM-174 board. polling complete. Boot configuration: Processor - 68040 Memory detected - 32M bytes Primary serial ports - 4 SCSI Controller press [space] to bypass memory test MEMORY TEST passed - 32M bytes TIMER TEST passed SERIAL PORT TEST Primary serial ports - 4 Port # 0 passed Port # 1 passed Port # 2 passed Port # 3 passed SCSI Controller Controller Self-test passed SCSI drive Self-test passed Find diagnostic cylinder passed Write/read test passed 40?! This is not a Eagle 300! We need to open this thing up. didn't expect was the CPU board on the bottom: it's a full 68040, just like the test said. Bclk for the bus clock, and then a second exactly twice as fast labeled Pclk as the so-called processor clock. In the 68020 and 68030, which accept a single clock signal, their internal ALU runs at only half the bus speed such that the 40MHz 68EC030 in the Eagle 300 actually has an ALU running at just 20MHz. For the 68040 Motorola removed this limitation to run its ALU at full bus speed, but the chip designers also studied trace data to optimize for the most common instructions, implementing a 7-stage pipeline where most effective address calculations and fetches occur with no penalty. The upshot of this was to cut most instructions' cycle count in half or more. The 2x processor clock is truly used, but not to speed up the ALU, so it's wrong to say the 68040 is "double-clocked." In a two-part 1990 article in IEEE Micro, Motorola explains that "Pclk provides timing reference points within each Bclk period," employing it for synchronous sub-cycle timing, and furthermore explicitly states that "all internal operations cycle at the Bclk rate." Still, the fact this clock signal was present at all gave Apple an excuse to tout both timings for '040 Macintoshes, and Alpha Micro cheerfully reported just the faster one, even though the chip only appears to run at that comparative speed from its larger 4K L1 cache, unhindered ALU and reduced cycles per instruction. On this board, Pclk is provided by the same 80MHz crystal we saw on the AM-172, divided down by two to yield Bclk in the same way the 68EC030 board does for its single 40MHz clock. (For the E400's AM-174-00, this crystal would have been 66MHz divided down to 33MHz.) The single parity FPM SIMM slot and 16MHz and 50MHz oscillators are also present, as well as the same 53C710 SCSI controller, here a genuine NCR part. However, new for this board is 32K of L2 cache at the upper right/northeast corner, implemented with two IDT 71B74 8K cache-tag RAMs together providing 29 bits (512MB) of address comparison, and then four Cypress CY78C185 8K SRAMs for the cache itself. Remember, however, that in AMOS all cache is I-cache. Effectively this 40/80MHz AM-174 makes our new system a full Eagle 500, so why is it labeled as "only" an E300? I don't how how these systems originally shipped from the factory nor whether the E500 designation actually appeared on the back, but Alpha Micro did run a promotion in late 1997 where you could upgrade any Roadrunner '030 to a Roadrunner '040 by swapping the CPU board. My guess is this is one of those systems, and because that upgrade kit wouldn't have changed the plate on the back itself, the reseller simply noted it to be a working E300 and sold it to me as such. While that wasn't exactly what I was hoping for, it's a really nice bonus, as evidenced by the system benchmark (taken on the SCSI hard disk it shipped with): That score officially makes it the new fastest Alpha Micro in the shop by far (this is where that prior 83.6 number came from, compared to the E450's 59.9 and E300's 36.9). It seemed a shame to carve it up as spare parts, so let's have a little look around. A surprising thing about the system configuration file (SYS:AMOS32.INI, a/k/a AMOS32.INI[1,4]) is that only one terminal is actually defined, which makes you wonder what those two RJ-21s are doing. Best guess is that this machine passed through a couple hands and ended its prior service as a single user's toy, much as my own machines are now. In a related phenomenon an annoyingly small number of terminal types are defined, all inconvenient, likely to save memory. On the other hand, the full AlphaTCP suite (based on SpiderTCP and SpiderStreams, best known as the Windows NT TCP/IP stack before 3.5) is defined and present, and we saw it running before, so we ought to be able to make use of that. That's its IP address, and we saw Telnet was running (TCP:TELNED.LIT), but there's a snag: The TCP:CONFIG. file sets up TCPEMU, AlphaTCP's master daemon, acting as its service description file (really a sort of up-script). This line for the Telnet daemon causes an automatic log-on from a Telnet session and inconveniently forces the FWYCOL terminal type, intended for a real Wyse WY-370. That's also the terminal setting for the single serial port which our "everything console" doesn't speak, meaning I can't visually edit anything without making a big mess. Ironically, that AM-75 colour head my E300 uses is itself a modified WY-370, but no longer speaks that terminal emulation either. But we have another option! Now knowing its IP address, I set up a small wired network to my MacBook Air. Although some terminal programs like Kermit 95 can speak WY-370 over Telnet, using MacWise — now freeware and runs on Tiger and Power Macs all the way to Tahoe on Apple silicon — I managed to discover that its TeleVideo 925 emulation will partially match FWYCOL. This was just enough to, with some care, edit TCP:CONFIG. and remove all the defaults so that I could manually select the terminal type on Telnet login. I then reset the FTP user credentials in TCP:FTPUSR., uploaded a copy of ANSI.TDV from the Eagle 450, and changed the TDVDEF line in a test copy of SYS:AMOS32.INI to allow it as a terminal type, which at last got me a proper session. I'm not sure of this system's most recent owner and the seller didn't know either, but there were two interesting domains in its TCP:HOSTS. file (identical to /etc/hosts) for that IP address. One domain name belongs to someone who appears to have been a recording engineer (!). This domain still exists, but is only partially functional, so I've chosen not to link it here. The other domain name was for a personal genealogy site, now parked, and that site appeared to be what it was currently offering via the Web server on HTM:, mapped to PPN [7,77]. However, because the HTML files were not on an extended directory device, I can't tell when the site was created because the old AM file system doesn't preserve modification times: Looking up this domain in the Wayback Machine, it seems to have been active in the early 2000s. Still, not bad assuming it was still serving it then. differently on the AM-172 and AM-174s, so it seemed safer just to never separate them in the first place. I wasn't even going to move the SSD and hard disk — since TCP/IP networking was already set up on the E500, I could just upload the site to the E500's hard disk instead. Only the E300's power supply, serial port cards and Tandberg QIC streamer would stay. Once the new transplanted system was working, then I'd see about doing the ZuluSCSI upgrade again. It tried to boot. Startled, I powered it off, connected the everything console, and turned it back on with the reset button held in to force a self-test. Except for the missing hard disk, it passed the self-test — even the SCSI controller tested good. I decided to push my luck and connect the ZuluSCSI, and this time it did throw a 2d. I freaked out again, powered it off, let it sit and then gingerly turned it back on. It still did it. However, when I disconnected the ZuluSCSI, it went back to trying to boot, and a repeated self-test was still clean. This was clearly new behaviour — beforehand, it wouldn't do anything, not even enter the self-test, let alone attempt booting. Recall that error 2d had two mentions in the manual, the second one referencing the primary and secondary boot devices. Since the self-test said the SCSI controller was fine, this made me suspicious that there was something about the ZuluSCSI's configuration the machine didn't like. Knowing some systems can get finicky about sluggish devices immediately after power-up, I reconnected the ZuluSCSI and powered the E300 back on. Code 2d duly appeared, but this time instead of turning it off, I hit the reset button just to see what it would do. SI.LIT had actually dropped to around 10, which was 33% slower than its original hard disk. .scsi Dispatched SCSI port: SCSI id 7: (reserved) SCSI bus host adaptor SCSI id 6: No device or unable to report SCSI id 5: No device or unable to report SCSI id 4: No device or unable to report SCSI id 3: SCSI-2 (tape) TANDBERG TDC 4200 =08:01CREATED12119 SCSI id 2: No device or unable to report SCSI id 1: No device or unable to report SCSI id 0: SCSI-2 (disk) ZULUSCSIHARDDRIVE 2.3BZuluSCSI .si dsk0: SI--AMOS System Information, (C) Copyright 1990, Alpha Microsystems Inc. Version 3.0(114) Computer Type: Eagle Main Processor: 68030 Co-Processor: None MMU present: Yes Operating System: AMOS 2.3A(484) Boot PROM revision: M00 Number of SSDs: 1 Total Memory: 16384 K-bytes Resident Monitor Memory: 6218 K-bytes Computing Index (CX), relative to AM-100: 36.8 48 bit FP Index (4X), relative to AM-100: 36.3 Floating Point Index (FX), relative to AM-100/L: 5.6 Disk Index (DX), relative to ST-506: 41.2 .si dsk0: SI--AMOS System Information, (C) Copyright 1990, Alpha Microsystems Inc. Version 3.0(114) Computer Type: Eagle Main Processor: 68030 Co-Processor: None MMU present: Yes Operating System: AMOS 2.3A(484) Boot PROM revision: M00 Number of SSDs: 1 Total Memory: 16384 K-bytes Resident Monitor Memory: 6218 K-bytes Computing Index (CX), relative to AM-100: 36.9 48 bit FP Index (4X), relative to AM-100: 36.3 Floating Point Index (FX), relative to AM-100/L: 5.6 Disk Index (DX), relative to ST-506: 100.4 six times faster than the original hard disk). There's no obvious caching in the Blaster's firmware that I could see, and although the AMOS system-wide disk cache was on, the test is supposed to be unaffected by OS-level caching. On the other hand, when I rebooted AMOS from software, its score went back to 41 and 100 thereafter. More on this phenomenon later. Termination power was sufficient to power the Blaster. But what the Blaster didn't fix, even with its later firmware, was the 2d code the E300 kept throwing from a cold start. Although booting after a manual reset was still fine, it refused to start after a power cycle without assistance, which I need it to be able to do if there's a power outage when I'm not around. .ini file, but since the issue appeared to be what the E300 thought was the secondary device and I knew the streamer was connected and working, I had a look at the back boot DIP switches first. The previous setting was #1 and #3 on, which would attempt a boot from the (non-existent) floppy drive first, and then try the SCSI hard disk. This had always worked before, but we have no floppy drive and I'm not likely to find an AM-219 anytime soon, so I tried the other boot position where it attempts a boot from the streamer first — again, known good electronics — by switching #4 on. And that worked: now it boots reliably. The conversion is complete. So, with the E300 resurrected and finally successfully converted to solid state, let's really push our luck here ... by doing the same with the others! (I should note I excluded the E100s this time around; I haven't found a special task or physical location for them yet.) SI.LIT disk score of 30.9. For this system I bought a ZuluSCSI Wide, which tops out at 34 MB/s read and 15 MB/s write, and which I already use in my Apple Network Servers with a similar NCR 53C825A. .scsi Dispatched SCSI port: SCSI id 15: No device or unable to report SCSI id 14: No device or unable to report SCSI id 13: No device or unable to report SCSI id 12: No device or unable to report SCSI id 11: No device or unable to report SCSI id 10: No device or unable to report SCSI id 9: No device or unable to report SCSI id 8: No device or unable to report SCSI id 7: (reserved) SCSI bus host adaptor SCSI id 6: No device or unable to report SCSI id 5: No device or unable to report SCSI id 4: No device or unable to report SCSI id 3: No device or unable to report SCSI id 2: SCSI-2 (tape) HP C1533A A907 9 SCSI id 1: No device or unable to report SCSI id 0: SCSI-2 (disk) ZULUSCSIHARDDRIVE 2.3WZuluSCSI .si dsk0: SI--AMOS System Information, (C) Copyright 1990, Alpha Microsystems Inc. Version 3.0(114) Computer Type: Eagle Main Processor: 68040 Co-Processor: None MMU present: Yes Operating System: AMOS 2.3A(484)-15 Boot PROM revision: D00 Number of SSDs: 1 Total Memory: 73728 K-bytes Resident Monitor Memory: 4366 K-bytes Computing Index (CX), relative to AM-100: 59.9 48 bit FP Index (4X), relative to AM-100: 58.2 Floating Point Index (FX), relative to AM-100/L: 7.9 Disk Index (DX), relative to ST-506: 51.0 .si dsk0: [...] Disk Index (DX), relative to ST-506: 52.2 . the last Alpha Micro article we booted our AM-1200 desktop system using a BlueSCSI attached to its external 50-pin Centronics-style SASI port, which with some tweaks to the settings file can be made compatible enough for its very old bus. But I'd like to use that device for other things, so we'll put the old ZuluSCSI card — more than fast enough for this thing — inside. To avoid me having to dismantle it every time I want to switch the disk image, I disconnected one of its unused serial ports and threaded a USB cable there. SUB0:. It's as fast as the BlueSCSI was, probably because both devices can easily saturate its slow SASI bus: AMOS 1.4 misdetects its 68010 as a 68000 (AMOS 2.3A detects the CPU correctly), but like the E450, there was no second test bump either. "everything console." I should point out that the E500 has a slightly later version of AMOS than the E300 and E450 and we get a little more explanation with the bus readout: While all three systems support synchronous SCSI, this version of AMOS actually reports it, along with the width (8-bit, because it's only Fast SCSI, not Wide). Now, what about performance? The ST32272N we replaced is a 7200rpm drive while the ST310151N in the E300 was only 5400rpm, thus accounting for the E500's better baseline disk score, and the E500's faster CPU probably explains its slightly higher first-run result of 46.3. But the second run is even more lopsided than what we saw with the E300, a massive 180.7. Since two of the systems were doing it, it was time to dig in a little more. The ZuluSCSI Blaster has a debug mode switch which echoes status and current operation to a connected machine over USB serial at 115200bps, so I hooked up both the E500's serial console and the USB port of the Blaster simultaneously to the MacBook Air to watch what SCSI operations occurred during the benchmark test. very unusual: there wasn't any. In fact, even though the baseline score had sunk down to a dismal 10 or so because of all the debugging data it was emitting, the second run was still a blistering 180.7. This really sounded like operating system caching, but except for the AM-1200 where I wasn't running it to save memory, the other three systems were running the standard disk cache as virtually any performance Alpha Micro would — and allegedly SI.LIT should be unswayed by it. For some reason the E450's cache configuration wasn't locking UFDs (user file directories, i.e., PPNs), but turning that off on the E500 and E300 didn't make a difference, and all three were caching everything else including the MFD (master file directory). However, when I turned off caching altogether at runtime (using the CACHE.LIT tool and disabling with OFF), though the scores got really low, they were now consistent from run to run. Do note our observation isn't enough to fully explain the E450, which also runs the OS disk cache, but didn't have a second run bump. If you disable the cache on the E450, although its overall score declines, it still remains consistent from run to run. One possibility is that the driver for its later SCSI controller just isn't as aggressive. Although the overall issue certainly calls into question how useful SI.LIT is as a disk benchmark, despite being likely the only Alpha Micro benchmark tool you can expect most recent systems to come with, the question then becomes how comparative disk performance on an Alpha Micro should be measured. Was it still fine for me to continue with my usual practice of leaving the cache on and testing immediately after boot? It's probable that there'd be little in the cache by that point, whatever would be there would likely be similar from system to system (assuming the same version of the operating system), and likelier still that whatever was present in the cache wouldn't be very germane to SI.LIT. Under my current knowledge of AMOS I think these numbers are still broadly comparable; I just have to keep in mind that the score on subsequent runs is likely to be bogus. precisely even with our E450 here). The E100 was discontinued shortly afterwards in 2001. SI.LIT CX score of 816 (!) on dual Athlon 2800+ CPUs. Later AM-113 SuperFalcons were just serial ports and a socket for the SSD; the main system was now more than suitable to service them, and Motorola wasn't making any more 5102 CPUs either. The Eagle 800 that same year ran the same technology in miniature, bringing the great experiment to a close with the discontinuation of the last E250 and E450 systems, and today's systems sold by the reformulated Alpha Microsystems LLC use the same x86-based architecture. Over their production lifetime, analysis of known serial numbers suggests around ten thousand 68K Eagles were ultimately sold, and now I've got five of them. Rest in peace, Rod. They're still great machines.

17 hours ago • 1 votes
from plesiochronous to synchronous

Up until the 1960s, telephone calls were carried as analog signals. There was a great deal of frequency division multiplexing onto open wire, and sometimes coaxial cables, but in the post-war period buried multipair telephone cables began to dominate. While the network was surprisingly functional—long distance calls from coast to coast were still quite intelligible—there were fundamental limitations to analog carriers. The quality of the call would degrade with each step, a problem that was much more acute for the sensitive digital connections that were already the apparent future of the telephone system. These were digital signals modulated over the top of telephone lines: the telephone system itself still handled them as analog calls, just ones that required special care for quality. During the Second World War, it became apparent that many of the long distance network's challenges could be greatly simplified by using digital signaling as the basis of the entire long distance system. In 1943, Bell Labs contributed to the design of an early encrypted voice communications system called SIGSALY. Unlike the "voice scramblers" and other analog techniques of the early phase of the war, SIGSALY was recognizably similar to modern encryption. The voice data was quantized into numerical values, so that digital arithmetic could be used to encode it. In the quest to build a cryptosystem for voice, Bell Labs incidentally built the first applied system for PCM digital voice transmission. After the war, it was an obvious next step to refine this technology and apply it to regular service. This didn't necessarily mean direct digital service, at least not at first: most subscriber loops remain analog to the present day. For the first years of the digital network, even the exchanges themselves switched purely analog signals. Between offices, though, encoding the analog channels into digital ones solved problems from audio quality, to microwave path overloading, to flexibility in traffic routing. Digitization became a core part of AT&T's concept of a carrier, just like frequency division multiplexing had in the 1920s. That's the background to the 1962 introduction of T-carrier, the first digital carrier in the commercial telephone system. The Digital Hierarchy Back in the article on T-carrier (which you should read if you are at all interested in this topic!), I explained that T1, T2, T3, etc. are hierarchically larger-bandwidth connections that carry the DS1, DS2, DS3, etc. digital signals respectively. Let's quickly review this hierarchical concept and, just to keep things interesting, add the further confusion of the D1 channel bank. A D1 channel bank is the physical piece of equipment that digitizes 24 voice channels and multiplexes them into a DS1 signal, which will be carried over T1 carrier. Similarly, four DS1 signals can be combined into a DS2 signal and carried over T2. Seven DS2 signals can be combined into a single DS3 signal and carried over T3. And so on, at least as far as they were able to get with the technology of the time. The DS2 level maps generally to the D2 channel bank, but that relationship can be confusing as the subsequently developed channel banks added features as well as capacity. The D2 channel bank could multiplex 96 channels onto a DS2 signal, but it also increased the overall quality of the audio channel to meet the technical requirements for general mixed toll service (the D1 had originally only been approved for conventional voice channels, not other applications like data-grade leased lines). In order to make that improvement, the D2 had to reduce the amount of quantization noise introduced into the signal. The D1 channel bank had used PCM audio with seven bits per sample, and calculations showed that eight bits per sample would be required for a toll-grade connection 1. In a way, that looked easy: DS1 already used 8-bit units. But the eighth bit, not used as part of the PCM sample, was reserved for signaling purposes. No bother: the D1 channel bank hadn't really used the full signaling bandwidth anyway, so it could comfortably be reduced to meet a compromise position. The D2 channel bank used 8 bits for most PCM samples, but every six frames the last bit of each sample was reserved for signaling use rather than audio. This created a slight increase in quantization noise, and a significant decrease in signaling rate, but the resulting numbers for both fell within acceptable margins. A perfect compromise, but one that feels technically inelegant: in a sense, the D2 channel bank used 7.833 bits per sample. Ouch. This must have given the Bell System's engineers similar discomfort, but the aesthetic offense was worth it for backwards-compatibility. By the time the D2 channel bank was fielded in 1970, there were a huge number of D1 channel banks and T1 connections in service. Backwards-compatibility to existing T1 lines was a firm requirement. The D2 multiplexed 96 channels, four times a D1, and could carry those channels over four T1 links—in fact, initially, this was only the option. T2 and subsequent carriers were not fielded until after the D2. This backwards compatibility provided an upgrade path, as well as a way to subdivide the 96-channel capacity. You might realize that it is also a completely alternate explanation of the "hierarchy." You might wonder why the multipliers at the levels of the hierarchy are so inconsistent—24, 4, 7, 6. Reading the thing about signaling bits in every six samples above might have stirred deep memories of a concept called "robbed-bit signaling," a general concept of stealing occasional bits for other purposes that was widely used with T1. These are all smells, indications that a system was not architected, but grown. The "digital hierarchy" of T1, T2, T3, etc. is not some elegantly-designed system, at least not nearly as much as T4-era Bell Laboratories Record articles like to make it sound. It's mostly a result of happenstance, as the history of the Dn channel banks shows: each new channel bank, the D3, the D4, was designed to carry many more channels than those before. They were also required to be backwards-compatible with the use of the older channel banks and carriers. This process of exponential improvement with backwards compatibility led to the digital hierarchy as a natural consequence, not as a designed outcome. Plesiochronicity This emergent concept of layers of successively larger muxes explains the digital hierarchy, but we now need to consider the problem of timing and synchronization. In any system based on digital sampling, you have to be aware at the point at which those samples are taken in order to combine multiple signals. Consider the scenario where several simultaneous phone calls are being combined into a single digital mux for transmission. When the phone calls reach a D1 (or D2, D3, etc) channel bank, it samples all of the inputs at an appropriate time to build its DS1 (or higher) output stream. Nothing about this case is particularly complicated, except that it does require that all of the calls involved in the digital stream are sampled under the control of the same clock. But what about cases where lines need to be digitized in different locations? Say you have two smaller offices in a city, that need to deliver inter-office traffic to a tandem in the next city over. An obvious approach would be to digitize the calls in each of their local offices, send them as digital signals to one of the offices (perhaps the city's main downtown exchange), and then combine all of those digital signals together to be sent to the tandem switch. Doing this with channel banks wasn't very desirable: you would actually need to convert the calls back to analog, and then back to digital, in order to relay them from the first digital leg to the second. The first digital telephone leads were like this: completely point-to-point, without the possibility of switching or routing in the digital realm. That wasn't so big of a limitation early on when all of the switches were analog and digital leads just made the lines between them denser, but it prevented the construction of the fully digital network that Bell Labs envisioned. What was clearly needed was some sort of mux that could take four DS1s, already in that form, and combine them into a DS2. Repeat this for the higher levels as well, and you have the basis of a system that can combine and separate traffic as needed to meet the level of demand between various offices. But what about the sample timing? If you have four DS1 streams from four different places, at least back in the 1970s, they are not going to be synchronized. The four D1 channel banks would each be operating on their own sample clocks, and because of the imperfection of the physical world some of these clocks would run slightly faster or slower than others. When our four DS1 streams arrived at our notional multiplexer, the four streams would be not only out of phase, they would actually run at slightly different speeds. We can imagine a lot of modern solutions to this problem, probably by buffering relatively large blocks of samples in memory and then transmitting out of that buffer. In the '70s, things weren't so simple: first, memory was expensive and slow compared to the bitrates involved, and second, the latency involved in such extensive buffering was not considered acceptable in a network that had previously been all-analog and thus had no more latency than that implied by the speed of light. The solution to this problem, the Plesiochronous Digital Hierarchy or PDH, is not easy to understand from our contemporary perspective. Many textbooks on networking and communications make some nod to PDH, but it's often short and cursory, and leaves you with a nagging feeling that the whole thing doesn't make much sense. PDH is hard to relate to for two reasons: first, it was developed to solve a set of problems that (due to broader technical advancements) we no longer have. Second, even within those constraints, it is a solution that has been obsoleted by newer approaches. PDH is not something that anyone would design today, and in retrospect, it's probably best to think of it as a hack. PDH was an interim measure, if you will, that bridged a gap between the development of digital channel banks and the later development of a good approach to synchronizing a digital network. It would have been more convenient if those two systems had been designed at the same time, but in practice, they weren't: digital long distance lines were in common use a solid decade before AT&T had a reasonable architecture for a digital network. You can already see that they were thinking through these requirements in the late 1960s, leading to the concept of the PDH as a hierarchical network architecture, but most of the actual technology to provide that network was built later, in the '70s and '80s. For a while, there was just, you know, something that worked. A duct tape fix. Conceptual bailing wire. So what even was the PDH? It is helpful to start by defining the word "plesiochronous," which every textbook has to do since the term was invented by Bell Labs to describe their design. I will spare the roots, but plesiochronous means "almost synchronized" or "approximately synchronized." And that's what the PDH was: it was a synchronous digital system with some slop. There was a little wiggle room. A DS1 signal is 1.544 Mbps, so if you multiplex four of them, you might expect 6.176 Mbps. You would be wrong: a DS2 is actually 6.312 Mbps. The overhead here comes from the plesio part of plesiochronous. To understand how that works, we should look at the design of the multiplexer from the beginning. AT&T practice would call a multiplexer from the DS1 level to the DS2 level an M12, for, well, multiplexer from 1 to 2. In practice M13s seem to have been more common equipment, which were often implemented internally as seven M12s feeding one M23. In any case, the M12 takes in four different DS1 streams and then bit-interleaves them, such that every 4th bit of the output is the next bit from one of the inputs. This is simple to reason about and has the advantage that virtually no buffering ever happens. But what if one of the inputs is running slightly fast, and another slightly slow? The first thing to know is that the output stream is intentionally slightly faster than all of the inputs. A typical PDH system allowed a 50 PPM plus or minus variation in the rate of the inputs, which means that the output had to be at least 50 PPM fast relative to the sum of the inputs. That way, the multiplexer will never "fall behind" because an input is providing bits faster than the output can interleave them. The other thing to know is that the multiplexer must perform a function known as either stuffing or justification. It is said that the term justification was introduced within AT&T because it sounded more technical, while stuffing was more popular with the actual engineers. Certain parts of each outgoing frame are defined (by standard) as stuffable, and they're sometimes called gaps. These are sections where the multiplexer is allowed to either include real data, or to include some nonsense data called stuffing bits (or, if you want to sound more technical, justification bits). By choosing to either include real data or stuffing in the gaps, the multiplexer can actually adjust the "real" output data rate up or down. That way, it converges onto the correct average (the sum of the inputs) despite the "timing noise" of the inputs varying slightly up and down in real speed. If that didn't make sense to you, here's a different way to think about the same thing: we can imagine the DS2 output frame as consisting of four "slots" into which the four DS1 input frames are inserted (this isn't quite intuitive because they are interleaved bit-by-bit, but the math works out the same). Under the PDH design, these slots are all slightly wider than the input frames, so that there is "free space" for the input frames to be shifted a bit in either direction or stretched out. That allows the multiplexer to "catch up" on a fast input or to "draw out" the frames from a slow input. Thinking about it in terms of a wide slot that you can place the input into as desired also gives more value to the term "justification:" you can shift bits around, or add extra space in between them, just like you would to justify text. The actual implementation of this idea is pretty complicated, because the demultiplexer on the other end needs to know which bits were stuffed so that they can be discarded. That's why PDH specified gaps at fixed positions: the control signaling included a set of flags that indicated whether the fixed gaps were used for data or stuffing. When the multiplexer decided whether or not to use a stuffable bit (based on whether or not the input was getting ahead of the output mux), it also had to store that decision for the end of the frame so that the flag could be set correctly. PDH wasn't an elegant solution, but it was a solution, and multiplexers were designed up to the DS4 level based on the PDH approach. We all owe PDH a huge thanks for providing the clunky stopgap that allowed the construction of a nationwide digital telephone network. That said, well, it had quite a few downsides. First, PDH was never all that well standardized. Researching PDH is tricky because there aren't a lot of documents that provide conclusive detail about the actual implementation in the US, mostly just summaries, conceptual sketches, and examples from Europe. As far as I can tell 2, Western Electric never really offered a standalone PDH multiplexer. Larger integrated systems from Western Electric (like, say, a microwave carrier equipment bay) might include a PDH multiplexer, but it's not clear to me if WE even made them—the advertisements and pricing information I can find for PDH multiplexers are from other vendors like Farinon and NEC. In North American practice, it doesn't seem like PDH was ever robustly standardized, and there were implementation differences between equipment from different manufacturers. That's probably one of the reasons for the scarcity of information on PDH equipment: you wouldn't usually buy a single PDH multiplexer, but instead would specify a specific model as part of a larger system. That meant that the PDH network was far less flexible than AT&T had hoped. The problem was even bigger when you looked overseas: AT&T tended to field the best thing that Bell Labs and Western Electric could come up with, and then promulgate that as a standard later on. The European telcos tended not to adopt new American developments until they were mature, giving them the opportunity to make standards before any equipment was in place. Thus the European versions of US telephone carriers are often a bit better, and PDH is no exception. In fact, the US, Europe, and Japan used three different hierarchies for PDH signaling. The European standard put more channels into a stream (the "E1" at about 2 Mbps), and used a consistent multiplier of 4 for each level. European PDH practices even got a complete published standard from CCITT. The Japanese system used the same DS1 standard as the US but with different multipliers (4, 5, 3, 4 instead of 4, 7, 6). PDH could thus not be used for international calling without completely remuxing everything. Another shortcoming of PDH had to do with that remuxing. The decision to bit-interleave the input frames together, stuff some bits, and then put flags on the bit stuffing into the interleaved stream, meant that a PDH multiplexer effectively "scrambled" the input streams. If you had, say, a DS3 stream and wanted to extract a single voice channel from it, you had a lot of work in front of you. It simply wasn't feasible with the electronics of the era to analyze the stream to determine the interleaving and stuffing bits at each level. Instead, you would have to put the DS3 through demuxers all the way back down to the bottom of the hierarchy, pull out the channel you wanted, and then remux it all the way back up. Pulling a single channel out of a DS3 could require two 11.5 foot tall equipment bays, and even further quantization noise due to the nature of the robbed bit scheme. Two things happened that pushed PDH to its limit: first, fiber optics. AT&T was famously a late adopter of fiber, and 1970s documents on the digital hierarchy talked about conveying it over T-carrier, microwave, and coaxial. We have talked about coaxial before, but L-carrier was an analog system. Digital coaxial did emerge as the T3, T4, and T5 carriers, but these were relatively short range and mostly only saw use on shorter inter-office links. Things were changing, though: by the time DS3 equipment was appearing in telephone exchanges, competitors were already evaluating fiber optics. AT&T dipped their toe in the water in 1976, with an experimental DS3 fiber optic link in Atlanta. The next year, both AT&T and competitor GTE had DS3 fiber lines in service (in Chicago and the LA area, respectively). Outside manufacturers started offering T1-over-fiber equipment around 1980, and in 1983 AT&T had a multimode fiber system stretching from New York to Washington. Sparing the (fascinating) story of the business and competitive factors, the writing was on the wall: fiber was the future of long lines. AT&T engineers realized that the PDH design did not scale well to the enormous capacities that fiber could reach, and even worse, that complex fiber networks would require exactly the capability that PDH lacked: extracting single channels from the mux. Besides, despite the systems in use, there really wasn't any standard for conveying PDH signals over fiber, which only meant more compatibility issues. Something else was coming over the horizon in 1983: divestiture. The complete reorganization of AT&T in the mid-1980s, breaking the Baby Bells up into independent companies, made the interoperability problems with PDH much more severe. Bell operating companies (BOCs) would need to easily exchange traffic with both AT&T Long Lines and, eventually, with other BOCs and competitive phone companies and long distance carriers. That was very difficult when that traffic was mostly conveyed in loosely-standardized black-box muxes. Many aspects of US telephony were loosely standardized prior to the Bell System breakup because AT&T ran the entire network and didn't really need to cooperate with others. After 1984, the US became a lot more like Europe: for technology to be widely adopted, it would need to have a well-defined, published standard. That meant a standards body—the part of AT&T that had formerly published the practices used by all of the BOCs, after divestiture, became an independent company called Bellcore. Bellcore had an immediate goal of developing a fully standardized set of digital interfaces to be used between telephone companies. At the same time, a group of carriers and equipment manufacturers organized an association to start an ANSI standards process for fiber-optic equipment. They had a goal of an industry standard for high-speed digital fiber optics, so that terminal equipment from different manufacturers could be used together. That effort formed the ANSI T1X1 working group, titled Digital Hierarchy and Synchronization. In 1985, Bellcore proposed their draft standard to the T1X1 committee: they called it SONET. Synchronicity First, I will briefly explain some context for my global readers. One of the shortcomings of PDH was its lack of international standardization, and SONET would not make the same mistake. Shortly after SONET was proposed to ANSI, it was also proposed to CCITT. For historic reasons, and despite the fact that they are largely identical, SONET (Synchronous Optical Network) is a North American standard while the European standard is called SDH, for Synchronous Digital Hierarchy. I will stick to the term SONET because it is catchier, but the name SDH better conveys that SONET is essentially the same idea as PDH but it's actually synchronous. Beyond that, it is also more feature-rich and much better standardized. SONET starts out higher up in the digital hierarchy. The "lowest" SONET level is Synchronous Transport Signal 1, or STS-1, which is typically transported by Optical Carrier 1, or OC-1. OC-1 operates at 51.84 Mbps, sufficient to carry a PDH DS3 channel with signaling overhead. By this point in telecommunications history, channel banks had improved and 50 Mbps wasn't what it once was, so DS3 was a good choice for the basic unit of a high-capacity transport system. This also provided critical backwards compatibility: DS3 was the most widely deployed PDH signal, so by accommodating a DS3 within an STS-1, SONET provided an easy route to tunneling legacy PDH links through a SONET replacement. I put a lot of time into writing this, and I hope that you enjoy reading it. If you can spare a few dollars, consider supporting me on ko-fi. You'll receive an occasional extra, subscribers-only post, and defray the costs of providing artisanal, hand-built world wide web directly from Albuquerque, New Mexico. By this time, the Digital Hierarchy principle from PDH was well established, so SONET follows the exact same pattern—but, fortunately, the naming convention improved. OC-3 is three times the bandwidth of OC-1, OC-12 is twelve times the bandwidth of OC-1, and so on, with OC-24, OC-48, OC-192, and even OC-768 in common use, the latter achieving over 38 Gbps of payload. SONET is complex compared to what you might expect, or at least different. A SONET frame does not consist of a header and a body. Instead, like with PDH, all of the data is interleaved together in a way that feels messy. In an STS-1 signal, for example, a frame consists of three bytes of protocol overhead and 87 bytes of payload, making up a 90 byte pattern that is repeated nine times to make up an 810 byte frame. Higher levels use larger frames but keep the same structure, effectively mixing the header into each frame. Like PDH's bitwise interleaving, this is done to manage latency: interleaving the header with the payload avoids the need to buffer an entire frame of data at any point. Because of this design decision, SONET can provide a latency that is less than the frame time. This description also oversimplifies by discussing only one part of SONET's protocol overhead. There are actually several layers of overhead, all interleaved together, that serve different purposes. The "lowest layer" overhead is recreated at each regenerator along the route (devices that decode and then reencode the optical signaling to compensate for path loss), while higher layers are only processed by terminal equipment and are used for purposes like connection quality monitoring. These types of overhead are important because, unlike PDH, SONET provides a specification for the entire physical infrastructure. SONET is virtually always used with one of a few standard architectures, each of which are designed to provide redundancy. SONET uses unidirectional signaling on the fiber, so any SONET link will have two fibers normally active, one for each direction. Where things vary are the approach to redundancy: in a linear route with automatic protection, there are two "spare" fibers. When terminal equipment detects (via monitoring protocol overhead signaling) that the connection quality is no longer good on the primary pair of fibers, it will switch automatically to the secondary pair. Particularly common with SONET though, and particularly distinctive, are rings. A unidirectional ring uses two fibers, one for each direction, but each of those fibers goes in a circle and returns to the origin point. At the origin point, each frame is transmitted both directions into the ring. The terminals elsewhere on the ring compare the two copies that they received, one from each direction, and use the better of the two. This provides protection against damage to one of the fibers: terminals that are "downstream" from a broken fiber should still be able to receive the frames that went the other way around. A downside to this protection method is that it is inefficient: sending every frame twice means that two fibers are still providing a theoretical maximum bandwidth equal to the capacity of a single fiber. Bidirectional rings use a more complicated monitoring scheme to detect when frames are lost, so that they do not need to preemptively send them twice. A broken fiber will be detected and, depending on the ring, either sent the other way around or sent on a second redundant fiber allocated to the same direction. SONET's ring architecture is one of its most interesting properties. Unidirectional rings are often used as the "last mile" component of a SONET network. Consider, for example, a large business in the early days of the internet. If you wanted 100 Mbps internet connectivity, one of your best options was to purchase SONET service from the telephone company. They would take an existing ring, starting and ending with a telephone exchange, and figure out how to add your premises somewhere along it. The ring shape allowed redundancy with a relatively efficient use of fiber length. Bidirectional rings, with their greater complexity but greater efficiency, were usually used between telephone exchanges where there was adequate room for more complex terminal equipment. The ring topology also illustrates the biggest advantage of SONET over PDH. Unlike PDH, SONET hierarchy levels keep overhead and frame data in the same position, with consistent offsets to the framing data necessary to find an individual channel. That means that it is practical to inspect a high-rate SONET link, find a specific channel, and pull out those bits. You can even find where a channel is located and then overwrite just those bits, inserting data into the stream. This pattern is called "add-drop multiplexing," because the multiplexer usually adds some channels and drops others. Let's work a concrete example. Many years ago, I was an intern at a General Electric plant in Virginia. Although the facility's main internet connectivity had since been upgraded, SONET service was still active for redundancy and for a legacy TDM PABX. In an equipment room, the telco-owned add-drop multiplexer sat in a rack with the fiber enclosures where the building was actually spliced into the ring. The multiplexer was the customer premises equipment, and our Cisco router was connected by OC fiber to the multiplexer's "low speed" side while its "high speed" side had two connections, one each way, to our "neighbors" on the ring. The telco had left a diagram taped to the rack that made this material: one pair of fibers went to the National Radio Astronomy Observatory, the other went to the National Geospatial Intelligence Agency. There must have been some interesting traffic on that ring! The multiplexer found the time slots in the SONET ring that were assigned to us, "dropped" them and sent those bits out on the low-speed side, and then received bits on the low-speed side that were "added" back into the same time slots. Each of the organizations along the ring did the same thing with their own channels. The fact that the SONET ring had to "pass through" each of its users was a material downside, since a failure of an add-drop multiplexer at one customer could cause service disruptions for others. That's why the multiplexer was telco property, and further why SONET rings were always deployed with some type of automatic protection and redundancy. Ultimately, though, a telephone company technician still showed up at the security desk from time to time, needing to perform maintenance or repairs on the SONET equipment. The ability to perform add-drop multiplexing at any point on a SONET link also enabled digital cross-connects. This, finally, was the original dream of the nationwide digital network: by pairing two add-drop multiplexers together, you can switch channels from one SONET line onto another, completely digitally and with almost no latency. Paired with a management protocol called Transaction language 1 (TL1) for remote reconfiguration, digital cross connects make the long distance network vastly more flexible. Big chunks of long distance traffic could be instantly rerouted, and consolidated onto major routes or spread across more minor ones. Now, you might wonder, what happened to the whole synchronization problem? The most obvious difference between PDH and SONET is that PDH is plesiochronous while SONET is synchronous. SONET requires that all of the terminals, at least within a given interconnected network, operate in synchronization. That's exactly the requirement that PDH rejected. What changed is the state of the art in timing and synchronization. A few things came together to obsolete the need for plesiochronous technology. One was the introduction of the Bell System Reference Frequency: to coordinate both analog and digital systems that were timing-sensitive, AT&T introduced a nationwide time synchronization system. By the 1980s, the 2048 kHz Bell System Reference Frequency was distributed to nearly all telephone facilities over the long distance infrastructure. Various time code systems followed. During the same period, GPS was coming online, and GPS receivers became a relatively inexpensive source of precise time. Most terminals on a SONET ring got their timing from the ring: they just followed the signal and added or dropped channels at the appropriate time. A "master" terminal, typically at the telephone exchange, had to maintain that beat. In the early days, it would likely derive its clock either from an atomic clock in the office or via a reference signal coming from another office. In the later days of SONET, the time source was more likely to be a "BITS" or Building Integrated Timing Supply. BITS are basically time servers for telephone offices, typically using a GPS receiver as their discipline source and transmitting a few different types of TDM time codes to whatever devices needed them. Despite all of these measures, it's interesting that SONET still cheats. There is a bit of wiggle room. Part of the SONET protocol overhead is a pointer that indicates the offset within the frame that the payload frame actually starts. SONET devices can thus start the payload a little bit late in a SONET frame, providing an opportunity to fix timing errors. This capability means that SONET is still an option even in plesiochronous environments, which eased the fairly rapid transition from PDH to SONET. SONET in Use SONET is fast enough to survive into the modern era, although it is now decidedly legacy. 38 Gbps is nothing to sneeze at, and still compares favorably with a lot of common long distance connections. Where SONET shows its age is in the actual network payload. What do you actually put on a SONET ring? Well, of course, you can fit a whole lot of 64 Kbps telephone channels onto any of the SONET tiers. You can do that using a "raw" T1 signal or via ISDN, which uses the same basic fiber signals but better standardizes telephone signaling (e.g. off hook status) and interoperability. But even an OC-1 has 672 channels, more phone calls than just about anyone needs to make. In the 1980s, the driver for commercial SONET service was data. SONET itself is incredibly protocol neutral—one of the reasons for its unusual framing structure is that it is agnostic to the size, rate, and timing of the frames of the next layer protocol. SONET basically treats all connections as opaque serial streams, so conceptually, you can put just about anything over it. Because SONET evolved during the same period as ISDN, the two are closely related. During the late '80s, as SONET was being deployed, the "Broadband ISDN" initiative created Asynchronous Transfer Mode (ATM). ATM is a virtual circuit switching protocol that multiplexes a large number of small, fixed-size packets (called cells) over high-speed connections like SONET. ATM was, at one time, a serious contender for the dominant protocol in wide area networks. That didn't come to be, though, and IP took over—but not before ATM was well-enough established to be a tempting way to carry IP. IP over ATM was fairly common, and in fact, remained the norm for DSL until fairly recently. There were other options as well: Packet over SONET was a standard for SONET-based PPP connections, which could carry just about anything but were often used for IP. A newer offering is Ethernet over SONET, which does exactly what it sounds like. The internet provided a compelling use-case for SONET, but it also became a challenge. SONET was designed within the telephone industry, which emphasized handling fixed-speed channels with very high reliability and very low latency. The IP-based internet embraced the opposite: opportunistically moving variable speed connections over relatively high-latency and low-reliability channels. That meant that internet service over SONET became awkward. For one, it tended to be slow. The reason was bandwidth allocation: getting time on a SONET ring means getting channels assigned, and you will pay for those channels 24/7 regardless of whether or not you use them. A technology called virtual concatenation was developed to allow SONET's lower-level building blocks (like STS-1) to be combined in arbitrary ways to produce just about any amount of bandwidth you could want, but every Mbps over SONET was an Mbps that couldn't be used by any other customer. You paid for it. Later on, variable-bandwidth allocation systems were developed, but they were complex and missed the opportunity to make much of a difference. SONET's fixed-bandwidth design made it excellent for real-time media, the wheelhouse of the telephone industry, but ironically real-time media was missing from the information revolution. Internet connections were generally too slow and unreliable to carry it. By the time real-time media returned, IP connections had gotten so fast that they could make up for the lack of traffic engineering with raw speed. Slow and reliable lost out to fast and unreliable almost every time. SONET also had all the typical warts of a telephone industry network architecture. By the '90s, IP-based network equipment was both cheaper and easier to use than ATM and SONET devices. Upstart internet service providers were much more competitively motivated, driving down prices on non-SONET services. SONET has been superseded as a standard by OTN, Optical Transport Network. OTN is an ITU standard for a digital hierarchy that starts at 2.5 Gbps and goes up, well, as high as you want. 800 Gbps "OTUC8" is now in service. Besides being faster, OTN is also designed to accommodate variable packet connections like Ethernet much more efficiently. And, of course, even OTN is losing the fight against schemes that carry Ethernet frames directly over fiber. Is this the reason that 8-bit bytes became so universal in computing? I genuinely do not know, this would be a good topic for future research. A long time ago, on a different blog, I spent some time trying to establish the lineage of the 8-bit word length that is now so universal. Without devoting a career of research to the topic, I think the best I can say is that there seem to have been several parallel paths that converged on 7 and 8 bits as good choices for communications purposes. Digital telephony equipment was certainly one of them.↩ And I would very much appreciate information from anyone who knows otherwise.↩

19 hours ago • 1 votes
Inside a 1980s filter chip that uses switched capacitors

Sometimes it's easier to identify an IC with a microscope. While sorting a box of old ICs, CuriousMarc came across some Harris ICs labeled "F1-10-5", a mysterious part number that didn't show up in any databooks. Since unidentifiable ICs are useless, he gave me one to analyze. Conveniently, it was in a ceramic package, so I could open it up with a quick tap from a chisel. Under the microscope, the chip's most striking feature was a grid of square capacitors. With all those capacitors, I guessed that it was a switched-capacitor filter. The die provided another clue: the part number HF-10. With this information, we quickly found that the chip was Harris's version of the standard MF10 switched-capacitor filter chip.1 The Harris integrated circuit, labeled F1-10-5 (or maybe FI-10-5), with a 1985 date code. Photo courtesy of CuriousMarc. Switched-capacitor filters were a popular way to implement analog filters in the 1980s. Rapidly switching capacitors in and out of a circuit enabled the construction of single-chip filters that were easy to use and performed well. The MF10, introduced by National Semiconductor in 1981, provides two flexible filters on a chip; each filter acts as a low-pass filter, band-pass filter, or a high-pass filter. The filter's characteristics are simple to control with a few external resistors. The Harris HF-10 die under the microscope with the main functional blocks labeled. (Click for a larger image.) Since I had the chip under the microscope, I took the opportunity to analyze it more closely. The white lines are the metal wiring that connects the chip's circuitry. Under the metal layer are two layers of polysilicon (reddish) and the underlying silicon (gray). The top and bottom halves of the chip are mostly mirror images, corresponding to the chip's two filters. The distinctive reddish squares in the middle of the chip are 72 tiny capacitors, constructed from polysilicon. Above the capacitors, CMOS switches turn on and off at the clock frequency, switching capacitors in and out of the circuit. Each filter uses three operational amplifiers (op amps), outlined in red. At the right are the three outputs from the three op amps: high pass, band pass, and low pass. The control circuitry is on the left: clock level shifting, clock shaping, frequency ratio handling, startup circuitry, and current sinks to provide fixed currents to other parts of the chip. Around the edges of the silicon die, 20 hair-thin bond wires connect the die to its 20 external pins. The die has some interesting chip art: a Harris logo and an outline of Florida; Harris was headquartered in Melbourne, Florida. The initials on the die are presumably the engineers who designed the chip. Some interesting images from the die. Switched capacitor circuits The filter is based on switched-capacitor circuits. A switched capacitor can replace a resistor in certain circuits, as shown below. The switches are controlled by a clock signal; the switches alternately close in clock phase 1 and phase 2 (ϕ1 and ϕ2). In phase 1, the capacitor is charged to the input voltage. In phase 2, the capacitor passes charge to the output. By rapidly toggling the switches, charge is (almost) steadily passed to the output. The larger the capacitance, the more charge that is passed through. Likewise, a higher frequency passes more charge. It can be shown that the circuit matches a resistor with resistance of 1/(fC): a higher capacitance and frequency correspond to lower resistance. A switched capacitor can replace a resistor. Why would you replace a simple resistor with this complicated switching circuit? In an integrated circuit, resistors are inaccurate and inconveniently large, especially high-value resistors. Replacing a large resistor with a small capacitor saves space on the die. Moreover, it is easy to generate an extremely accurate clock frequency with an inexpensive quartz crystal, making the filter's frequency highly accurate. Finally, the equivalent resistance can be changed simply by changing the clock frequency, making it easy to tune or sweep the filter. On-chip capacitors are fairly inaccurate, with the capacitance typically varying by 20% from chip to chip due to variations in manufacturing conditions. However, this isn't a problem in the MF10 because the circuitry was designed to depend on the ratio between capacitances, which is stable. Specifically, the MF10 uses 72 identical square capacitors, which will have almost identical capacitances. Careful examination shows that some of the capacitors are separate, while others are connected in groups of 8 to form larger capacitors.2 This yields a highly accurate ratio of 8:1 between the grouped capacitors and the individual capacitors, even though the absolute capacitance will vary from chip to chip. Each capacitor is constructed from two layers of polysilicon,3 forming the plates of the capacitor, separated by a thin layer of insulating oxide that acts as the dielectric. I estimate that each capacitor square is 5 picofarads. The grid of capacitors in the MF10. I've added yellow lines to show how the capacitors are grouped. The switches are above and below the capacitors. This chip uses one more trick with switched capacitors: it inverts the voltage while acting as a resistor. In the switched-capacitor circuit below, there are four switches. The capacitor charges to the input voltage during phase 1, the same as before. But duing phase 2, note that the top plate of the capacitor is grounded, while the output comes from the bottom plate. If the capacitor was charged to, say, 1 volt, the top plate is 1 volt above the bottom plate. So if the top plate is grounded, then the bottom plate must be at -1 V. (This is the same idea as a charge pump.) This circuit turns out to yield a more accurate filter because some parasitic capacitances cancel out. By using four switches, the switched capacitor can invert the voltage. The op-amp integrator The heart of most analog circuits is the operational amplifier, or op-amp. An op-amp takes two inputs and amplifies the difference by many orders of magnitude. Normally, an op-amp is configured with negative feedback, which forces the two inputs to be essentially the same. Op-amps are useful not only for amplification, but for filtering, buffering, summing, and other tasks. A basic op-amp integrator. The filter chip uses op-amps as integrators, to integrate an input voltage over time. The circuit above shows a simple op-amp integrator. The input voltage produces a current that flows through the resistor and charges the capacitor, so the capacitor holds the integral of the input voltage over time. You might expect that the left side of the capacitor would become positive as it charges. However, the op-amp's feedback forces both inputs to ground, so instead the right side of the capacitor becomes negative. Thus, the output is the negative integral.4 The MF10 chip uses the circuit above, except the resistor is replaced with a switched capacitor. The capacitor across the op-amp is not switched, but consists of either 8 or 16 capacitors from the capacitor grid. The CMOS switches The CMOS switch is the technology that makes the switched-capacitor filter possible. A CMOS switch has a fairly low resistance (maybe tens of ohms) when closed and an enormously high resistance (hundreds of megohms) when open. This high resistance ensures that the charge doesn't leak out of the capacitors. A CMOS switch is constructed by combining an NMOS transistor and a PMOS transistor. The NMOS transistor and PMOS transistor are opposites. An NMOS transistor is good at pulling the output low, while a PMOS transistor is good at pulling the output high, so in combination they provide an effective switch. An NMOS transistor is turned on by a high voltage on the gate, while a PMOS transistor is turned on by a low voltage on the gate. Thus, a CMOS switch requires two control signals of opposite polarity, which is a minor inconvenience. A CMOS switch. The diagram above shows how a switch is implemented with an NMOS transistor and a PMOS transistor in parallel. When the control line is high, and the inverted control line is low, both transistors turn on, providing a path through the switch circuit. When the control line is low (and the inverted line high), the transistors turn off, opening the switch. The chip uses CMOS switches in pairs, with one switch on and the other off. This forms the equivalent of a toggle switch that connects either A or B to the output. This circuit is simply two CMOS switches, with separate control lines for each switch, as shown below. In the MF10, the switch toggles at the clock frequency. During one clock phase, the switch is connected to A, while the switch is connected to B during the other clock phase. The schematic on the right, below, is the same circuit, but reorganized to match the layout on the die. A double-throw CMOS switch. The photo below shows a CMOS switch on the die, constructed from two PMOS transistors and two NMOS transistors. The four control lines run horizontally in polysilicon, forming a transistor gate where they cross doped silicon. The upper PMOS and NMOS transistors are driven by the clock phase 1 (Φ1) signals, while the lower transistors are driven by the phase 2 signals. CMOS switches on the die. The metal layer was removed to show the transistors. One problem with switched-capacitor filters is that the clock can generate switching noise that appears in the chip's outputs. The MF10 uses several techniques to reduce clock noise. Each set of transistors is surrounded by two isolation rings: one positive and one negative. These block noise from traveling through the silicon substrate. Note that the rings have opposite polarity for the NMOS transistors and the PMOS transistors. The light tan region in the photo above is a second layer of polysilicon. This polysilicon is connected to ground, providing a shield layer over the switching circuits. For the photo above, I removed the metal layer with acid5 to make the transistors more visible. The photo below shows the original die, with the metal layer connecting the transistors. The small black circles are connections between the metal layer and silicon or polysilicon. The same CMOS switches, showing the metal layer. Putting it together: the state variable filter There are many ways of creating a filter. The MF10 chip uses a technique called the state variable filter, invented in 1967. This circuit acts as three filters, with high-pass, band-pass, and low-pass outputs. Moreover, the circuit is flexible since the frequency, the gain, and the filter quality (Q) can be varied independently. It uses three op-amps: one to sum signals and two for integration. By changing how the values are summed, the characteristics of the filters can be changed. The diagram below shows a simplified representation of a state variable filter. The mathematics behind a state variable filter is complicated, so I won't get into it. In short, the signal, the integral, and the double integral form the three state variables that define the state of the system. Simplified diagram of a state variable filter, with two integrators. Inspired by North Coast Synthesis. The block diagram below shows how the filter is represented in the MF10 datasheet.6 The diagram is similar to the diagram above, with three op-amps. However, the summing circuitry has been separated out. Moreover, the feedback paths are not shown explictly. Instead, resistors are connected between the chip's external pins (squares) to configure the filter as desired. The mode switch at the top allows the low-pass feedback to be controlled by an external pin (SA/B). Block diagram of one of the filter sections. Adapted from the datasheet. The schematic below is my reverse-engineered schematic of the filter, as implemented on the chip. It closely matches the block diagram, but fills in the details. In the block diagram, the summing circuit (circle) adds one signal and subtracts two signals. This summing circuit is implemented with the three switched capacitors on the left, which act as summing resistors. Note that one switch is grounded during phase 1, while the others are grounded during phase 2; switching the polarity implements addition versus subtraction. The top sum input is either feedback from the low-pass output or ground, selected by an input pin. A CMOS switch is used here, but the switch is static, not clocked, so it doesn't use protection rings and shielding like the other switches. My reverse-engineered schematic of one of the filters. Click this image (or any other) for a larger version. The integrators have switched capacitors on the inputs, acting as resistors. The integration capacitor is either 8 or 16 "squares" of capacitance, selected by a ratio selection pin. This controls the ratio between the clock frequency and the filter frequency, either 50:1 or 100:1.7 Although the integration capacitors are attached to a CMOS switch, the switch is static, so the capacitors act as regular capacitors, not switched capacitors. The op-amps The op-amps are fairly standard CMOS op-amps, built from about 35 transistors. (You might get a lower count if you try counting the transistors below, since some of the blocks are multiple transistors.) The op-amp transistors are much larger than the CMOS switch transistors (very bottom, center). On the die, each op-amp is split into two parts: the differential amplifier on the left and an additional amplification stage on the right. A large capacitor (pinkish) sits between the halves. My first thought was that this was the integration capacitor, but it is just a frequency compensation capacitor, common in many op-amps to stabilize the output. The op-amps also have large transistors next to the output pins; these transistors are functionally part of the op-amps, but located next to the pins to minimize resistance. One of the chip's op-amps. I removed the metal layer to make the transistors visible. One unusual feature of the op-amps is a low-power mode. Pulling a particular IC pin low causes the chip to stop filtering and enter a low-power mode, reducing power consumption by 70%. This is implemented by shutting down the "current mirror" circuits that provide fixed currents to the op-amps and other parts of the chip. The non-overlapping clock generator The MF10 chip is driven by external clock signals, one for each filter, with the frequency of the filter proportional to the clock frequency. The photo of the CMOS switches earlier showed that the clock drives four control lines for the switches. You might think that two control lines would be sufficient: the clock and the inverted clock. The problem is that it is very important to avoid having both switches closed at the same time, even for a moment, as that will short the inputs and corrupt the signals. Instead, the two switches have separate control lines that enforce a small gap between when one switch opens and the other one closes. This is implemented with the circuit below that takes an input clock signal and produces the four outputs that drive the switches. The circuit to generate non-overlapping clock signals. There is a delay between when gate A or B turns on and when the corresponding output changes. The idea behind the circuit is that a phase is blocked from going high until after the other phase goes low, with a pair of inverters providing additional delay. In more detail, suppose the input clock drops from high to low. Gate A will turn off, causing the phase 1 output (ϕ1) to drop after a few gate delays (A delay). Gate B can't turn on until ϕ1 goes low. After additional gate delays, ϕ2 goes high. The behavior is similar when the input clock goes high. Gate B turns off, causing ϕ2 to go low after a delay. This allows gate A to turn on, turning on ϕ1 after more delay. To summarize, after a phase is turned off, there is a delay before the other phase turns on, so the two phases never overlap. The clock-shaping circuitry is implemented with CMOS logic gates. The photo above shows this circuitry under the microscope, with the metal layer removed. The rectangular blocks are doped silicon that forms transistors. The darker regions on the left are NMOS transistors and the lighter regions on the right are PMOS transistors. A CMOS gate consists of NMOS and PMOS transistors working together. The PMOS transistors are larger because PMOS transistors are slightly less efficient than NMOS transistors. The dark circles are contacts between the silicon and the metal layer on top. The copper-colored lines are not metal but a special type of silicon called polysilicon. When a polysilicon line crosses doped silicon, it forms the gate of a transistor. The pinks and greens are due to thin-film interference from a thin layer of oxide that didn't completely dissolve; the silicon is actually gray. The ternary input A weird feature of the chip is the input pin that selects the ratio between the input clock and the filter frequency. In effect, this is a digital input with three values. Tying the pin to the high supply voltage selects a 50:1 ratio. Tying the pin to the midpoint between the supply voltages selects a 100:1 ratio. Pulling the pin to the low supply voltage stops the filter and puts the chip into a low-power mode.8 To handle the three-level input, the input goes through two separate buffers, one that transitions at a lower voltage and one that transitions at a higher voltage. Thus, the two buffers separate the middle signal level. Each buffer consists of a special inverter feeding into a regular inverter. Before explaining the special inverters, I'll review how a regular CMOS inverter works. A CMOS inverter is constructed from a PMOS transistor and an NMOS transistor. When the input is high, the NMOS transistor turns on and pulls the output to ground. When the input is low, the PMOS transistor turns on and pulls the output high. Thus, the input signal is inverted. A CMOS inverter is constructed from a PMOS transistor and an NMOS transistor. In the die photo, you can see the four PMOS transistors (light gray) and four NMOS transistors (darker), forming four inverters. When a polysilicon line (copper-colored) crosses a doped silicon region, it forms the gate of a transistor. For this picture, I dissolved the metal layer in acid so the transistors are visible. The metal layer connected the transistors to complete the wiring of the inverters: it connects the two "out1" contacts to "in2" and connects the two "out2" contacts to the rest of the chip. For the second buffer, "out3" connects to "in4" and so forth. The four inverters that handle the ternary input. I flipped the image to make the orientation better. In this circuit, the length of the transistor gates is varied to make the inverters activate at different voltage levels. Six of the transistor gates are normal (orange arrows); the PMOS gates are wider (in the vertical direction) than the NMOS gates because PMOS transistors are inherently weaker. However, two of the transistor gates are unusually long (horizontal direction, red), making the transistors weak since the current must travel a longer distance. The inverter on the left has a weak PMOS transistor. If the input is high or low, the inverter will operate normally. But if the input is in the middle, both transistors will partially turn on. Since the PMOS transistor is very weak, the NMOS transistor will "win", pulling the output low. Thus, the leftmost inverter treats a medium-level input as a 1, outputting a 0. The third inverter is the opposite; the NMOS transistor has a long, winding gate, so it is weak. In this case, a medium-level input will partially turn on both transistors, but the PMOS transistor will "win", pulling the output high. To summarize, the two inverters have opposite behavior for a middle-level signal, allowing the three input levels to be distinguished. Since the output from a special inverter may be weak, the output goes to a normal inverter to amplify the signal. Conclusions Like most semiconductor companies, Harris has a complicated history. Harris started way back in 1895 as a printing press company. Harris moved into high technology in the 1950s and 1960s, acquiring various radio and electronics companies. In particular, Harris entered the IC business in 1967, when it acquired Radiation, Inc., renaming it Harris Semiconductor a few years later. (We've encountered some Radiation modules in Apollo systems, but I haven't written about them yet.) Harris got out of the semiconductor business in 1999, spinning off Intersil, which was later acquired by the Japanese semiconductor firm Renesas. In 2019, Harris merged with L3 Technologies to become L3Harris, the eighth-largest defense contractor in the US. As for switched-capacitor filters, they have lost popularity as filtering is now more easily done in the digital domain. Texas Instruments acquired National Semiconductor (and the MF10) in 2011; TI's website shows the MF10 as active but expensive and out of stock, so it's probably no longer being manufactured. State variable filters are still used in the synthesizer world both because of their flexibility and because they provide low-pass, band-pass, and high-pass filters in one unit. For more, follow me on Bluesky (@righto.com), Mastodon (@[email protected]), or RSS. Thanks to CuriousMarc for providing the IC. AI statement: Despite the presence of the em dash, no AI was used in the writing of this article (details). Notes and references Once we found the "HF-10" part number, a search turned up a National Semiconductor databook that confirmed that the Harris HF-10 was a direct replacement for the National Semiconductor MF10. It remains a mystery why the Harris chip is externally labeled "F1-10-5" rather than "HF-10". This format doesn't resemble other Harris part numbers. I would suspect a military part number, but it is completely different from the military formats that I've seen on other chips, such as JM38510 numbers or NSN numbers. ↩ You might wonder why the larger capacitors are formed by connecting eight smaller capacitor squares, rather than making one capacitor that is eight times as big. The reason is to get better matching between the two capacitor sizes. A capacitor that is eight times as large won't have exactly eight times the capacitance due to factors such as the behavior of the electric field around the edge of the capacitor, inaccuracies that may make the capacitor slightly larger or smaller than desired, or etching variability around the edges. By building larger capacitors out of identical smaller capacitors, the values can match very well, up to ±0.01% according to The Art of Analog Layout. (With laser trimming, matching of ±0.001% is possible, but that is much more accuracy than the MF10 required.) ↩ Most chips from this era have a single layer of polysilicon, so I was surprised to find two layers in this chip. I've seen two layers of polysilicon before, in the MK4116 DRAM chip and AMD's LANCE Ethernet chip. In both cases, the second layer of polysilicon was used for storage devices. ↩ A standard op-amp integrator is an inverting integrator, and the output is negative. However, the MF10 uses the four-switch switched capacitor that inverts the input voltage. The two negatives cancel out, so the MF-10's integrator is a non-inverting integrator. See Introducing the MF10: A Versatile Monolithic Active Filter Building Block for details. ↩ To remove the metal layer, I used Whink rust stain remover (1.5-3.5% HF) to remove the oxide layer and hydrochloric acid to dissolve the metal. I applied Whink for 20 minutes and HCl for 16 minutes in total. I alternated each chemical for about 3 minutes each, applying a few drops at a time. I examined the die under the microscope after each application to gauge the progress. I stopped at this point since the metal was removed and the underlying transistors were visible. Moreover, the silicon became differentially stained, with NMOS transistors significantly darker than PMOS transistors. Some more Whink would probably improve the appearance of the die, but the risk is that the polysilicon might get removed, which would be bad for reverse engineering. In other words, I'd rather stop too early than destroy the features that I want to see. ↩ For reference, the full block diagram of the chip is below, from the datasheet. Block diagram of the MF10 from the Texas Instruments datasheet.  ↩ The filter frequency of the MF10 can be set to either the clock frequency divided by 50 or divided by 100. You might wonder where these ratios come from, since the capacitors on the chip are in 8:1 or 16:1 ratios, not 50:1 or 100:1. The formula for a switched-capacitor integrator is that the filter frequency is the clock frequency divided by 2π times the capacitor ratio. (This can be derived from the op-amp integrator formula and the equivalent resistance of a switched capacitor.) It turns out 2π×8 is 50.27 and 2π×16 is 100.5, providing the 50 and 100 values. Note that these values aren't exactly 50 and 100; they are off by 0.5%. Curiously, the datasheet specifies that the typical frequency error is ±0.2%, significantly smaller. I suspect that the explanation is that the capacitor ratio is not precisely 16:1, due to stray capacitance in the wiring and other factors, and the designers ensured that these factors tweaked the ratio in the desired direction. ↩ I suspect that the ternary input pin was used because the chip didn't have enough physical pins for all the functions they wanted. Note that the two filters are entirely independent, even with separate clocks, except for the 50/100 ratio control and the A/B mode control. I'm sure that these two functions would have independent control pins if the chip had pins available. They could have used a standard 24-pin package for the chip rather than the somewhat unusual 20-pin package, but maybe they had a motivation for avoiding a much larger 24-pin package. ↩

2 days ago • 1 votes
Radxa's Q8B has 2x the performance and expansion of the Pi 5

There was a time I'd look at a board like the Radxa Dragon Q8B (at left, above) and be like, "there's no way I'd spend $209 on an SBC with 8 gigs of RAM". But we're in 2026, and seeing the 8 gig Raspberry Pi 5 going for almost the same amount, I figured I'd give it a shot. On paper, the Q8B beats the Pi 5 in pretty much every way. A lot of that is thanks to this Snapdragon 8cx Gen 3 chip, which is the same chip I tested on Microsoft's Windows Dev Kit 2023.

3 days ago • 1 votes
Three years later

Reflections on October 7th

4 days ago • 1 votes
📚 BoredReading

You seem to be enjoying this.

Join free to unlock everything.

Create free account

Already have an account? Sign in