This project implements a Triple DES (3DES) encryption system in a minimal embedded stack, using:
- A
VHDL IP corefor the 3DES algorithm (RTL-level) - A
Memory-Mapped I/O (MMIO)interface to connect the crypto block to a custom SoC based on HF-RISC - A UCX/OS microkernel driver, running on top of this simulated hardware platform
The work was developed using minimal versions of both HF-RISC and UCX/OS, provided by the course instructor, Prof. Sรฉrgio Johann.
The driver enables encryption and decryption of arbitrary-length messages via system calls (write(), read()) and supports the following block cipher modes:
- ECB (Electronic Codebook)
- CBC (Cipher Block Chaining)
- CTR (Counter Mode)
The system includes: PKCS#7 padding; Task ID management; Validation using real text data.
For a detailed explanation of the logic, experimental setup, and results, please refer to the ๐full technical report*
For detailed electrical characteristics, communication protocols, and performance specifications, refer to the official datasheet
- Overview of this work
- Important files
- Validation
- MMIO Register Interface for a 3DES VHDL IP Core
- How to Set Up the Environment
- How to run
The objective of this work was to implement and validate a device driver for encryption based on the Triple DES (3DES) algorithm on the UCX/OS operating system, running on the HF-RISC-E processor. For this purpose, a 3DES cryptographic core described in hardware (VHDL) and mapped via MMIO on the SoC bus was used.
The application communicates with the driver through the standard calls write() (to encrypt or decrypt data) and read() (to retrieve the result). The driver implements task control, access permission verification by Task ID, and supports the ECB (Electronic Code Book) mode. Tests were conducted with real messages and padding management following the PKCS#7 standard.
Additionally, optimizations were made in the communication with the cryptographic IP, including checking the READY signal before reading the output, thus avoiding race conditions. The driver's structure allows modularity for future modes (CBC, CTR) and ensures data integrity in the encrypted input/output process. The main application was used to test the complete encryption and decryption pipeline, validating the correctness of the implemented algorithm.
- minimal-HF-RISC-V/riscv/sim/tdes_tb.vhd -> HF-RISC-V System Testbench with Memory-Mapped 3DES Crypto Core and Peripheral Simulation
- minimal-HF-RISC-V/devices/controllers/tdes/* -> RTL hardware description, provided by the vendor
- minimal-HF-RISC-V/sim/rv32e_basic/debug.txt -> Debug
- ucx-os-minimo/app/tdes_driver/* -> Tests applications & triple-DES driver using read and write interface
The validation process was carried out using the hexdump function, which displays the binary content of a file in hexadecimal and ASCII format for low-level inspection.
The output (stdout) is saved in the debug.txt file, accessible from this location.
This project implements a bidirectional, memory-mapped I/O interface between a 3DES hardware encryption core, described in VHDL, and a RISC-V processor running firmware written in low-level C. The interface allows the CPU to configure control signals, load keys and plaintext blocks, and retrieve encrypted output โ all through mapped addresses in the processor's memory space.
This pattern follows the classical memory-mapped I/O architecture, where:
- Software performs reads/writes to fixed memory addresses (via
volatile uint32_t*in C). - Hardware (VHDL) listens to those addresses and either:
- Latches the written value into internal signals.
- Returns current status or result values during a read.
This design decouples the processor from the internals of the crypto core, and provides a clean interface contract between the software and hardware domains.
minimal-HF-RISC-V/riscv/sim/tdes_tb.vhd
-- 3DES Core
data_read_des <= data_read_des_s(7 downto 0) & data_read_des_s(15 downto 8) & data_read_des_s(23 downto 16) & data_read_des_s(31 downto 24);
ext_periph <= '1' when address(31 downto 24) = x"e7" else '0';
-- Read Process: Maps 3DES outputs to processor's readable addresses
process (clock_in, reset, address, data_out_des, function_select_des, reset_des, ldkey_des, lddata_des, out_ready_des)
begin
if reset = '1' then
data_read_des_s <= (others => '0');
elsif clock_in'event and clock_in = '1' then
if (ext_periph = '1') then -- 3DES at 0xe7000000
case address(7 downto 4) is
when "0000" => -- Control Register Read: 0xe7000000
-- Provides status of control signals, especially out_ready at bit 0
data_read_des_s <= x"000000" & "000" & function_select_des & reset_des & ldkey_des & lddata_des & out_ready_des;
when "0001" => -- Not readable, write-only
data_read_des_s <= (others => '0');
when "0010" => -- Not readable, write-only
data_read_des_s <= (others => '0');
when "0011" => -- Not readable, write-only
data_read_des_s <= (others => '0');
when "0100" => -- Not readable, write-only
data_read_des_s <= (others => '0');
when "0101" => -- Not readable, write-only
data_read_des_s <= (others => '0');
when "0110" => -- Not readable, write-only
data_read_des_s <= (others => '0');
when "0111" => -- Not readable, write-only
data_read_des_s <= (others => '0');
when "1000" => -- Not readable, write-only
data_read_des_s <= (others => '0');
when "1001" => -- Output Data Read [0]: 0xe7000090
data_read_des_s <= data_out_des(32 to 63);
when "1010" => -- Output Data Read [1]: 0xe70000A0
data_read_des_s <= data_out_des(0 to 31);
when others =>
data_read_des_s <= (others => '0');
end case;
end if;
end if;
end process;
process (clock_in, reset)
begin
if reset = '1' then
key1_in_des <= (others => '0');
key2_in_des <= (others => '0');
key3_in_des <= (others => '0');
data_in_des <= (others => '0');
function_select_des <= '0';
reset_des <= '0';
ldkey_des <= '0';
lddata_des <= '0';
elsif clock_in'event and clock_in = '1' then
if (ext_periph = '1' and data_we /= "0000") then
case address(7 downto 4) is
when "0000" =>
function_select_des <= data_write_periph(4);
reset_des <= data_write_periph(3);
ldkey_des <= data_write_periph(2);
lddata_des <= data_write_periph(1);
when "0001" => -- 0xe7000010
key1_in_des(32 to 63) <= data_write_periph;
when "0010" => -- 0xe7000020
key1_in_des(0 to 31) <= data_write_periph;
when "0011" => -- 0xe7000030
key2_in_des(32 to 63) <= data_write_periph;
when "0100" => -- 0xe7000040
key2_in_des(0 to 31) <= data_write_periph;
when "0101" => -- 0xe7000050
key3_in_des(32 to 63) <= data_write_periph;
when "0110" => -- 0xe7000060
key3_in_des(0 to 31) <= data_write_periph;
when "0111" => -- 0xe7000070
data_in_des(32 to 63) <= data_write_periph;
when "1000" => -- 0xe7000080
data_in_des(0 to 31) <= data_write_periph;
when others =>
NULL;
end case;
end if;
end if;
end process;
crypto_core: entity work.tdes_top
port map(
key1_in => key1_in_des,
key2_in => key2_in_des,
key3_in => key3_in_des,
function_select => function_select_des,
data_in => data_in_des,
data_out => data_out_des,
lddata => lddata_des,
ldkey => ldkey_des,
out_ready => out_ready_des,
reset => reset_des,
clock => clock_in
);This header file defines the memory-mapped register interface for a Triple DES (3DES) hardware crypto core. It allows a processor (e.g., RISC-V with UCX/OS) to interact with the hardware by reading and writing specific addresses corresponding to control, keys, input data, and output ciphertext.
The macros in this file represent physical register addresses, casted as volatile uint32_t*, to ensure direct and safe hardware access.
ucx-os-minimo/app/tdes_driver/tdes_driver.c
#include <ucx.h>
#include <device.h>
#include "tdes_driver.h"
#define DES_BASE 0xe7000000
#define DES_CONTROL (*(volatile uint32_t *)(DES_BASE + 0x000))
#define DES_KEY1_1 (*(volatile uint32_t *)(DES_BASE + 0x010))
#define DES_KEY1_2 (*(volatile uint32_t *)(DES_BASE + 0x020))
#define DES_KEY2_1 (*(volatile uint32_t *)(DES_BASE + 0x030))
#define DES_KEY2_2 (*(volatile uint32_t *)(DES_BASE + 0x040))
#define DES_KEY3_1 (*(volatile uint32_t *)(DES_BASE + 0x050))
#define DES_KEY3_2 (*(volatile uint32_t *)(DES_BASE + 0x060))
#define DES_IN0 (*(volatile uint32_t *)(DES_BASE + 0x070))
#define DES_IN1 (*(volatile uint32_t *)(DES_BASE + 0x080))
#define DES_OUT0 (*(volatile uint32_t *)(DES_BASE + 0x090))
#define DES_OUT1 (*(volatile uint32_t *)(DES_BASE + 0x0A0))Important
Implementation Details and MMIO Access in the uKernel-OS:
See ๐๐๐๐๐UCX README for complete details โ omitted here for clarity
MMIO (Memory-Mapped I/O) is a technique where hardware devices are controlled using memory addresses.
Instead of using special I/O instructions, the CPU communicates with hardware (like PCIe devices) by reading from and writing to specific memory addresses. These addresses donโt point to RAM โ they point to device registers.
How it works:
- The hardware logic listens to certain address ranges.
- The CPU accesses these like normal memory (e.g.,
*(volatile uint32_t*)0x40000010 = 0xFF;) - The device reacts by latching data, starting operations, or sending back status.
Think of it like controlling machines by writing values on specific โwallsโ inside a room: each โwallโ triggers a unique function.
-
Benefits:
- Simple interface (same as RAM access)
- Fast (no I/O instruction overhead)
-
Requires care:
- You must avoid caching
- Usually accessed via
volatilepointers in C/C++
This makes the CPU act like itโs writing to memory โ but itโs actually sending a signal to a hardware device.
- In three steps:
Clone -> Docker Setup-> Starting the container
git clone --recurse https://github.com/bbzaffari/MMIO-tripleDes-VHDL-coreFor the purposes of this class, the professor provided a pre-configured Docker environment that includes all the necessary dependencies for the system to run properly. You can download and set it up using the following command:
docker pull ghcr.io/sjohann81/linux-es
docker run -it --name SE -v "$PWD":/home ghcr.io/sjohann81/linux-es-
docker run is more than just "starting a container." Itโs a compound command that:
-
- Creates a new container from the specified image (ghcr.io/sjohann81/linux-es) โ this is like taking a blueprint (image) and building a working instance (container).
-
- Allocates resources like filesystem layers, network, and isolated namespaces.
-
- Configures the container with options like terminal interaction (-it), naming (--name), volume mounting (-v), and more.
-
- Runs the container by executing its default entrypoint (in this case, a shell environment).
-
-
-it: Allocates an interactive terminal so you can use the command line inside the container. -
--name SE***: Names the containerSE, which helps you reference it easily later. -
-v "$PWD":/home: Mounts your current working directory (from your host machine) into the container at the path/home.
Warning
- 1. After the first run, do not repeat the docker run command, or you'll create duplicate containers.
-
- This can lead to:
-
- Confusion (multiple containers with similar configurations)
-
- Conflicts over mounted volumes
-
- Wasted disk space and dangling containers
-
- 2.
$PWDis an environment variable that returns your current working directory.- That directory must contain the files you previously cloned from the project repository.
- Inside the container, those files will be accessible at
/home.
Accessing the Graphical Interface โ I do not recommend โ
Note
By default, the container launches a lightweight graphical environment, which can be accessed via your browser at: http://localhost:8080 This interface provides a minimal desktop environment running inside the container.
If you prefer to work directly via terminal or automation scripts, you can safely skip the graphical interface.
To do so, press Ctrl + C in the current terminal to exit the graphical session, and proceed with the next step in the terminal.
Once you've created the container using docker run, you don't need to recreate it again.
Instead, follow these two simple commands to reuse the container:
docker start SEThis command starts an existing container that has already been created earlier using docker run.
SEis the name you assigned to the container when you first ran it.- This command starts the container in the background โ it doesnโt attach to its terminal.
docker exec -it SE bashThis command attaches a terminal to the running container, allowing you to interact with it just like you would with a regular Linux shell.
-it: Opens an interactive terminal session.SE: Refers to the container you started.bash: Launches the Bash shell inside the container.
Note
- Use
start + execevery time you want to return to the container after rebooting or closing Docker โ neverdocker runagain for this purpose. - Together, these two commands will bring you back into your development environment inside the container โ ready to compile, run, and edit your project files that were mounted in
/home.
- It is assumed that the files are located in the
/homedirectory. - Sometimes
debug.txtis automatically deleted, but simply running the command again resolves it. - Validation was performed using
debug.txt, with output inspected viahexdump.
First, make sure the following tools are installed on your system:
apt update
apt install ghdl gtkwavechmod +x home/run_model.shcd home/./run_model.sh ECBValidation EBC โ minimal-HF-RISC-V/sim/rv32e_basic/debug.txt:
./run_model.sh CTRValidation CTR โ minimal-HF-RISC-V/sim/rv32e_basic/debug.txt:
./run_model.sh CBCValidation CBC โ minimal-HF-RISC-V/sim/rv32e_basic/debug.txt:


