├── .github └── workflows │ └── build_book.yml ├── .gitignore ├── .vscode └── settings.json ├── Dockerfile ├── README.md ├── meta-info.toml ├── misc └── cover_image │ ├── CircuitsandCode_SKDP.psd │ └── CircuitsandCode_SKDP │ └── CircuitsandCode_SKDP_3d.jpg ├── scripts ├── build_book.py ├── build_docker_image.py ├── build_requirements.json ├── build_svg_images.py ├── check_code_snippets.py ├── e-series.py ├── format.py ├── python_requirements.txt ├── setup.py └── util.py └── src ├── 64-bit-timer.tex ├── adc_init.tex ├── appendix_more _opamps.tex ├── appendix_more_passives.tex ├── appendix_packet_parsing_tests.tex ├── appendix_switched_divider.tex ├── bitstream_parity.tex ├── buck_vs_ldo.tex ├── cabin.sty ├── code ├── adc_init │ ├── CMakeLists.txt │ ├── adc_conversion.c │ ├── adc_init.c │ ├── adc_init_header.h │ └── adc_init_registers.h ├── bitbang_spi │ ├── CMakeLists.txt │ ├── bitbang_ex.c │ ├── bitbang_ex.h │ ├── bitbang_spi.c │ └── bitbang_spi_header.h ├── bitstream_parity │ ├── CMakeLists.txt │ ├── bitstream_parity.c │ ├── bitstream_parity.h │ ├── count_ones_bk.c │ ├── count_ones_bk.h │ └── count_ones_simple.c ├── keyword │ ├── CMakeLists.txt │ ├── main.c │ └── volatile_ex.c ├── packet_parsing │ ├── CMakeLists.txt │ ├── checksum_ex.c │ ├── main.c │ ├── packet_parsing_header.h │ └── parsing_implementation.c ├── pid │ ├── CMakeLists.txt │ ├── pid_controller.c │ └── pid_typedef.h ├── stack │ ├── CMakeLists.txt │ ├── main.c │ └── stack_ex │ │ ├── CMakeLists.txt │ │ └── main.c └── timer │ ├── CMakeLists.txt │ ├── timer_64bit_correct.c │ ├── timer_64bit_naive.c │ └── timer_registers.h ├── compare-i2c-spi.tex ├── current_sense.tex ├── diagram_generator ├── buck_ccm.py ├── buck_dcm.py ├── hpf_bode_plot.py ├── i2c.py ├── i2c_comparison.py ├── pid_discretization.py ├── pid_discretization_derivative.py ├── pid_discretization_integral.py ├── plot_template.py ├── priority_inheritance_input.json ├── priority_inversion_input.json ├── rtos_task_diagram.py ├── scope-trigger.py ├── step_response_plotter.py ├── step_responses.json └── wavedrom │ ├── can_bus_arbitration.json │ ├── can_dominant_recessive.json │ ├── diff_digital_signal.json │ ├── i2c_data_format.json │ ├── packet_parsing.json │ ├── parity.json │ ├── pwm.json │ └── spi_waveform.json ├── images ├── I2C_controller-target.png ├── cover.jpg ├── diff_pair_noise_immunity.png ├── svg │ ├── SPI_three_slaves.svg │ ├── can_bus_arbitration.svg │ ├── can_dominant_recessive.svg │ ├── diff_digital_signal.svg │ ├── i2c_data_format.svg │ ├── packet_parsing.svg │ ├── parity.svg │ ├── pwm.svg │ └── spi_waveform.svg ├── wikipedia_can_bus_topology.png └── wikipedia_diff_signal.png ├── interrupts.tex ├── keyword.tex ├── led.tex ├── main.tex ├── mutex_vs_semaphore.tex ├── opamp.tex ├── oscilloscope.tex ├── packet-parsing.tex ├── passives.tex ├── pid.tex ├── preamble.tex ├── references.bib ├── signalling.tex ├── spi-bitbang.tex ├── stack-growth.tex ├── switching.tex ├── title.tex ├── voltage_divider.tex └── what-is-can.tex /.github/workflows/build_book.yml: -------------------------------------------------------------------------------- 1 | name: Build Book 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | jobs: 12 | build-book: 13 | runs-on: ubuntu-latest 14 | 15 | container: 16 | image: sahilkale1/fw_hw_book_docker_img:latest 17 | 18 | steps: 19 | # Step 1: Checkout the repository 20 | - name: Checkout repository 21 | uses: actions/checkout@v3 22 | 23 | # Step 2: Build the book 24 | - name: Build the book 25 | run: python3 scripts/build_book.py 26 | 27 | # Step 3: Archive the built book 28 | - name: Archive built book 29 | if: success() 30 | run: | 31 | mkdir -p artifacts 32 | cp output/main.pdf artifacts/ 33 | 34 | # Step 4: Upload artifact 35 | - name: Upload artifact 36 | uses: actions/upload-artifact@v4 37 | with: 38 | name: built-book 39 | path: artifacts 40 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | *.log 3 | *.pdf 4 | *.aux 5 | *.fdb_latexmk 6 | *.fls 7 | *.synctex.gz 8 | 9 | __pycache__/ 10 | fw-hw-questions-venv/ 11 | generated_images/ 12 | build/ 13 | output/ 14 | 15 | .vscode/c_cpp_properties.json 16 | .vscode/launch.json 17 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "latex-workshop.latex.tools": [ 3 | { 4 | "name": "latexmk", 5 | "command": "latexmk", 6 | "args": [ 7 | "-synctex=1", 8 | "-interaction=nonstopmode", 9 | "-file-line-error", 10 | "-pdf", 11 | "-outdir=%OUTDIR%", 12 | "%DOC%" 13 | ], 14 | "env": {} 15 | }, 16 | { 17 | "name": "lualatexmk", 18 | "command": "latexmk", 19 | "args": [ 20 | "-synctex=1", 21 | "-interaction=nonstopmode", 22 | "-file-line-error", 23 | "-lualatex", 24 | "-outdir=%OUTDIR%", 25 | "%DOC%" 26 | ], 27 | "env": {} 28 | }, 29 | { 30 | "name": "xelatexmk", 31 | "command": "latexmk", 32 | "args": [ 33 | "-synctex=1", 34 | "-interaction=nonstopmode", 35 | "-file-line-error", 36 | "-xelatex", 37 | "-outdir=%OUTDIR%", 38 | "%DOC%" 39 | ], 40 | "env": {} 41 | }, 42 | { 43 | "name": "latexmk_rconly", 44 | "command": "latexmk", 45 | "args": [ 46 | "%DOC%" 47 | ], 48 | "env": {} 49 | }, 50 | { 51 | "name": "pdflatex", 52 | "command": "pdflatex", 53 | "args": [ 54 | "-synctex=1", 55 | "-interaction=nonstopmode", 56 | "-file-line-error", 57 | "%DOC%" 58 | ], 59 | "env": {} 60 | }, 61 | { 62 | "name": "bibtex", 63 | "command": "bibtex", 64 | "args": [ 65 | "%DOCFILE%" 66 | ], 67 | "env": {} 68 | }, 69 | { 70 | "name": "rnw2tex", 71 | "command": "Rscript", 72 | "args": [ 73 | "-e", 74 | "knitr::opts_knit$set(concordance = TRUE); knitr::knit('%DOCFILE_EXT%')" 75 | ], 76 | "env": {} 77 | }, 78 | { 79 | "name": "jnw2tex", 80 | "command": "julia", 81 | "args": [ 82 | "-e", 83 | "using Weave; weave(\"%DOC_EXT%\", doctype=\"tex\")" 84 | ], 85 | "env": {} 86 | }, 87 | { 88 | "name": "jnw2texminted", 89 | "command": "julia", 90 | "args": [ 91 | "-e", 92 | "using Weave; weave(\"%DOC_EXT%\", doctype=\"texminted\")" 93 | ], 94 | "env": {} 95 | }, 96 | { 97 | "name": "pnw2tex", 98 | "command": "pweave", 99 | "args": [ 100 | "-f", 101 | "tex", 102 | "%DOC_EXT%" 103 | ], 104 | "env": {} 105 | }, 106 | { 107 | "name": "pnw2texminted", 108 | "command": "pweave", 109 | "args": [ 110 | "-f", 111 | "texminted", 112 | "%DOC_EXT%" 113 | ], 114 | "env": {} 115 | }, 116 | { 117 | "name": "tectonic", 118 | "command": "tectonic", 119 | "args": [ 120 | "--synctex", 121 | "--keep-logs", 122 | "--print", 123 | "%DOC%.tex" 124 | ], 125 | "env": {} 126 | } 127 | ], 128 | "latex-workshop.latex.outDir": "../output", 129 | "editor.wordWrap": "on", 130 | "cSpell.words": [ 131 | "Anduril", 132 | "behaviour", 133 | "bytestream", 134 | "deasserted", 135 | "Kernighan's", 136 | "malloc", 137 | "microcontroller", 138 | "microcontrollers", 139 | "MOSFET", 140 | "op-amp", 141 | "prescaler", 142 | "Puratich", 143 | "QPPI", 144 | "reflectometry", 145 | "Sahil", 146 | "setpoint", 147 | "Skydio", 148 | "timestep", 149 | "transceive", 150 | "UART", 151 | "WARG" 152 | ], 153 | "files.associations": { 154 | "bitbang_ex.h": "c", 155 | "timer_registers.h": "c" 156 | }, 157 | "cmake.ignoreCMakeListsMissing": true, 158 | "python.terminal.activateEnvironment": true, 159 | "latex-workshop.chktex.enabled": false 160 | } -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Base image 2 | FROM python:3.10-slim 3 | 4 | # Set environment variables 5 | ENV PYTHONUNBUFFERED=1 \ 6 | DEBIAN_FRONTEND=noninteractive 7 | 8 | # Set working directory 9 | WORKDIR /app 10 | 11 | # Copy repository into the Docker image 12 | COPY . /app 13 | 14 | # Install system-level dependencies 15 | RUN apt-get update && apt-get install -y --no-install-recommends \ 16 | && apt-get clean && rm -rf /var/lib/apt/lists/* 17 | 18 | # Run setup.py to install dependencies 19 | RUN pip install --upgrade pip && \ 20 | python3 scripts/setup.py --no-sudo 21 | 22 | # Default command (optional, if you want to use this image interactively) 23 | CMD ["bash"] 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Circuits & Code Book Source 2 | Circuits and Code offers over 20 interview-style questions and answers designed for embedded software and electrical engineering interns. Written by two authors with experience hiring and mentoring interns for embedded software and electrical engineering roles, it draws on their academic background and industry expertise to focus on the concepts that matter most to interviewers. 3 | 4 | ## Download 5 | - Official PDF Download Link: https://circuits-and-code.github.io/ 6 | 7 | ## Development Instructions 8 | 9 | ### Setup (Ubuntu, Non-Docker, VSCode-Style) 10 | - Run WSL 11 | - Setup sshkey 12 | - Clone repo 13 | - Run `code .` in VsCode to open the repo in WSL 14 | - Download `Code Spell Checker` from VSCode Extension Marketplace 15 | - Download `LaTeX Workshop` from VSCode Extension Marketplace 16 | - Use a `venv` environment for this project, `source /bin/activate` 17 | - Run `python3 scripts/setup.py` to install everything required for building 18 | 19 | Note: There is a `DOCKERFILE` that can also be used to compile the book (and is used in CI). However, we strongly recommend the above workflow with VSCode for development as it's a lot more interactive, even though it involves installing system packages. 20 | 21 | ### Building 22 | - Run `python3 scripts/build_book.py` 23 | 24 | Note: it's also possible to: 25 | - Open and save `main.tex` to compile the entire directory 26 | - Open and save `{filename}.tex` to compile an individual file 27 | 28 | However, this will not generate images or check code snippets. 29 | 30 | ## Internal Tracking Documents 31 | - Initial Notes: https://docs.google.com/document/d/1Akp2vB-8zvOjKRvKKtQ4xRd1HW5HwOgYOPMeZlsEUBo/edit 32 | - Progress Tracking Sheet: https://docs.google.com/spreadsheets/d/1r3ARHjxrXqaiC7ljZWcs_fc2r5LI8fe7ycGm_sgxs3k/edit?gid=0#gid=0 33 | - Internal Splitwise Finance: https://secure.splitwise.com/#/groups/76688172 -------------------------------------------------------------------------------- /meta-info.toml: -------------------------------------------------------------------------------- 1 | [Title] 2 | Circuits and Code 3 | 4 | [Description] 5 | Circuits and Code offers over 20 interview-style questions and answers designed for embedded software and electrical engineering interns. Written by two authors with experience hiring and mentoring interns for embedded software and electrical engineering roles, it draws on their academic background and industry expertise to focus on the concepts that matter most to interviewers. 6 | 7 | With clear, concise explanations, coding snippets, and sample circuit diagrams, this guide provides a practical resource for motivated students preparing for co-op interviews. The complete, thoughtfully designed questions are ideal for interview practice, helping you build confidence in areas like firmware development, control systems, and hardware design. Whether you’re tackling your first technical interview or refining your skills, this book offers the tools to succeed. -------------------------------------------------------------------------------- /misc/cover_image/CircuitsandCode_SKDP.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/circuits-and-code/circuits-and-code-book/7cf76d1bb2a4bd97b9f964fc579af797cd4f7f2d/misc/cover_image/CircuitsandCode_SKDP.psd -------------------------------------------------------------------------------- /misc/cover_image/CircuitsandCode_SKDP/CircuitsandCode_SKDP_3d.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/circuits-and-code/circuits-and-code-book/7cf76d1bb2a4bd97b9f964fc579af797cd4f7f2d/misc/cover_image/CircuitsandCode_SKDP/CircuitsandCode_SKDP_3d.jpg -------------------------------------------------------------------------------- /scripts/build_book.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | import os 3 | import shutil 4 | import click 5 | import check_code_snippets 6 | import argparse 7 | 8 | # Constants 9 | OUTPUT_DIR = "output" 10 | GENERATED_IMAGES_DIR = os.path.abspath("src/generated_images") 11 | IMAGE_GENERATION_COMMANDS = [ 12 | f"python3 src/diagram_generator/rtos_task_diagram.py src/diagram_generator/priority_inversion_input.json --output_path={GENERATED_IMAGES_DIR}/priority_inversion.png", 13 | f"python3 src/diagram_generator/rtos_task_diagram.py src/diagram_generator/priority_inheritance_input.json --output_path={GENERATED_IMAGES_DIR}/priority_inheritance.png", 14 | f"python3 src/diagram_generator/pid_discretization_integral.py --output_path={GENERATED_IMAGES_DIR}/pid_discretization_integral.png", 15 | f"python3 src/diagram_generator/pid_discretization_derivative.py --output_path={GENERATED_IMAGES_DIR}/pid_discretization_derivative.png", 16 | f"python3 scripts/build_svg_images.py --input-dir=src/images/svg/ --output-dir={GENERATED_IMAGES_DIR}/svg_generated", 17 | f"python3 src/diagram_generator/buck_ccm.py --output_path={GENERATED_IMAGES_DIR}/buck_ccm.png", 18 | f"python3 src/diagram_generator/buck_dcm.py --output_path={GENERATED_IMAGES_DIR}/buck_dcm.png", 19 | f"python3 src/diagram_generator/i2c.py --output_path={GENERATED_IMAGES_DIR}/i2c.png", 20 | f"python3 src/diagram_generator/i2c_comparison.py --output_path={GENERATED_IMAGES_DIR}/i2c_comparison.png", 21 | f"python3 src/diagram_generator/step_response_plotter.py --input_file=src/diagram_generator/step_responses.json --output_path={GENERATED_IMAGES_DIR}", 22 | f"python3 src/diagram_generator/scope-trigger.py --output_path={GENERATED_IMAGES_DIR}/scope-trigger.png", 23 | f"python3 src/diagram_generator/hpf_bode_plot.py --output_path={GENERATED_IMAGES_DIR}/hpf_freq_response_plot.png", 24 | ] 25 | 26 | 27 | def remove_and_create_dir(directory_path): 28 | """Remove the directory if it exists, then recreate it.""" 29 | if os.path.exists(directory_path): 30 | click.secho(f"Removing existing {directory_path} directory...", fg="yellow") 31 | shutil.rmtree(directory_path) 32 | os.makedirs(directory_path) 33 | 34 | 35 | def run_command(command): 36 | """Run a shell command and handle its output.""" 37 | click.secho(f"Running command: {command}", fg="green") 38 | process = subprocess.Popen( 39 | command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True 40 | ) 41 | 42 | # Print stdout in real-time 43 | for line in process.stdout: 44 | print(line.strip()) 45 | process.stdout.close() 46 | 47 | # Wait for process to finish and handle errors 48 | return_code = process.wait() 49 | if return_code != 0: 50 | click.secho("An error occurred:", fg="red") 51 | for line in process.stderr: 52 | print(line.strip()) 53 | raise RuntimeError("Command failed: {}".format(command)) 54 | 55 | 56 | def generate_images(): 57 | """Generate images using predefined commands.""" 58 | remove_and_create_dir(GENERATED_IMAGES_DIR) 59 | for command in IMAGE_GENERATION_COMMANDS: 60 | run_command(command) 61 | 62 | 63 | def execute_latex_build(tex_path): 64 | """Compile the LaTeX document using latexmk.""" 65 | output_dir = os.path.abspath(OUTPUT_DIR) 66 | latexmk_command = ( 67 | f"latexmk -pdf -pdflatex=pdflatex -interaction=nonstopmode " 68 | f"-synctex=1 -output-directory={output_dir} {tex_path}" 69 | ) 70 | 71 | os.chdir("src") # Change to the source directory 72 | try: 73 | run_command(latexmk_command) 74 | click.secho("PDF successfully compiled!", fg="green") 75 | except RuntimeError: 76 | click.secho("Failed to compile PDF.", fg="red") 77 | raise 78 | finally: 79 | os.chdir("..") # Return to the original directory 80 | 81 | 82 | def main(tex_paths_to_build=None): 83 | """Main entry point of the script.""" 84 | if tex_paths_to_build is not None: 85 | remove_and_create_dir(OUTPUT_DIR) 86 | generate_images() 87 | 88 | if tex_paths_to_build is not None: 89 | for tex_path in tex_paths_to_build: 90 | execute_latex_build(tex_path) 91 | check_code_snippets.check_all_code_snippets() 92 | 93 | 94 | if __name__ == "__main__": 95 | parser = argparse.ArgumentParser(description="Build the book") 96 | parser.add_argument( 97 | "--only-gen-images", 98 | action="store_true", 99 | help="Only generate images", 100 | default=False, 101 | ) 102 | 103 | args = parser.parse_args() 104 | 105 | tex_paths = ["main.tex"] 106 | 107 | if args.only_gen_images: 108 | tex_paths = None 109 | 110 | main(tex_paths) 111 | -------------------------------------------------------------------------------- /scripts/build_docker_image.py: -------------------------------------------------------------------------------- 1 | import os 2 | import argparse 3 | 4 | IMAGE_NAME = "fw_hw_book_docker_img" 5 | 6 | 7 | def main(args): 8 | # Build the Docker image 9 | os.system(f"docker build -t {IMAGE_NAME} .") 10 | 11 | if args.upload: 12 | # Upload the Docker image 13 | os.system(f"docker tag {IMAGE_NAME} {args.user}/{IMAGE_NAME}") 14 | os.system(f"docker push {args.user}/{IMAGE_NAME}") 15 | 16 | 17 | if __name__ == "__main__": 18 | parser = argparse.ArgumentParser(description="Build Docker Image") 19 | 20 | parser.add_argument( 21 | "--upload", action="store_true", help="Upload the image to the registry" 22 | ) 23 | parser.add_argument("--user", type=str, help="Docker username", required=True) 24 | 25 | args = parser.parse_args() 26 | 27 | main(args) 28 | -------------------------------------------------------------------------------- /scripts/build_requirements.json: -------------------------------------------------------------------------------- 1 | { 2 | "apt": [ 3 | "git", 4 | "cmake", 5 | "clang-format", 6 | "texlive", 7 | "latexmk", 8 | "texlive-latex-extra", 9 | "texlive-fonts-recommended", 10 | "build-essential", 11 | "texlive-fonts-extra", 12 | "texlive-bibtex-extra", 13 | "biber", 14 | "python3-tk" 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /scripts/build_svg_images.py: -------------------------------------------------------------------------------- 1 | from PIL import Image 2 | import os 3 | import argparse 4 | from cairosvg import svg2png 5 | 6 | 7 | def crop_image(input_path, output_path): 8 | # Open the image file 9 | with Image.open(input_path) as img: 10 | # Convert image to RGBA to handle transparency if needed 11 | img = img.convert("RGBA") 12 | 13 | # Get the bounding box of the non-white area 14 | bbox = img.getbbox() 15 | 16 | if bbox: 17 | # Crop the image to the bounding box 18 | cropped_img = img.crop(bbox) 19 | 20 | # Save the cropped image 21 | cropped_img.save(output_path) 22 | print(f"Image successfully cropped and saved to {output_path}") 23 | else: 24 | print("No content found in the image to crop.") 25 | 26 | 27 | def convert_svg_to_png(input_dir, output_dir): 28 | # Ensure the output directory exists 29 | os.makedirs(output_dir, exist_ok=True) 30 | 31 | # Search for all .svg files in the input directory 32 | for root, _, files in os.walk(input_dir): 33 | for file in files: 34 | if file.lower().endswith(".svg"): 35 | svg_path = os.path.join(root, file) 36 | png_filename = os.path.splitext(file)[0] + ".png" 37 | temp_png_path = os.path.join(output_dir, f"temp_{png_filename}") 38 | final_png_path = os.path.join(output_dir, png_filename) 39 | 40 | # Convert SVG to PNG 41 | try: 42 | print(f"Converting: {svg_path} -> {temp_png_path}") 43 | with open(svg_path, "rb") as svg_file: 44 | svg_data = svg_file.read() 45 | svg2png( 46 | bytestring=svg_data, 47 | write_to=temp_png_path, 48 | output_width=1024, 49 | output_height=1024, 50 | ) 51 | 52 | # Crop the PNG after conversion 53 | crop_image(temp_png_path, final_png_path) 54 | 55 | # Remove the temporary uncropped PNG 56 | os.remove(temp_png_path) 57 | 58 | print(f"Successfully converted and cropped: {final_png_path}") 59 | except Exception as e: 60 | print(f"Failed to convert {svg_path}: {e}") 61 | 62 | 63 | if __name__ == "__main__": 64 | parser = argparse.ArgumentParser(description="Convert SVG images to PNG format.") 65 | parser.add_argument( 66 | "--input-dir", 67 | required=True, 68 | help="Path to the directory containing SVG files.", 69 | ) 70 | parser.add_argument( 71 | "--output-dir", 72 | required=True, 73 | help="Path to the output directory where PNG files will be saved.", 74 | ) 75 | 76 | args = parser.parse_args() 77 | 78 | input_directory = args.input_dir 79 | output_directory = args.output_dir 80 | 81 | convert_svg_to_png(input_directory, output_directory) 82 | -------------------------------------------------------------------------------- /scripts/check_code_snippets.py: -------------------------------------------------------------------------------- 1 | CODE_SNIPPET_PATHS = [ 2 | "src/code/pid", 3 | "src/code/bitbang_spi", 4 | "src/code/bitstream_parity", 5 | "src/code/keyword", 6 | "src/code/adc_init", 7 | "src/code/packet_parsing", 8 | "src/code/timer", 9 | "src/code/stack", 10 | "src/code/stack/stack_ex", 11 | ] 12 | 13 | RUNNABLE_PATHS = [ 14 | "src/code/packet_parsing/build", 15 | "src/code/keyword/build", 16 | "src/code/stack/build", 17 | "src/code/stack/stack_ex/build", 18 | ] 19 | 20 | import subprocess 21 | import shutil 22 | import os 23 | import click 24 | import argparse 25 | 26 | 27 | def check_code_snippet(wd_path): 28 | CMD = [ 29 | "rm -rf build", 30 | "mkdir -p build", 31 | "cd build && cmake ..", # Combine cd and cmake for clarity 32 | "cd build && make", 33 | ] 34 | 35 | for command in CMD: 36 | result = subprocess.run( 37 | command, 38 | shell=True, 39 | check=False, # Allow handling of return code manually 40 | cwd=wd_path, 41 | stdout=subprocess.PIPE, 42 | stderr=subprocess.PIPE, 43 | ) 44 | 45 | if result.returncode != 0: 46 | click.secho(f"Error executing: '{command}' in {wd_path}", fg="red") 47 | click.secho(result.stderr.decode(), fg="red") 48 | return False # Stop execution on failure 49 | 50 | return True 51 | 52 | 53 | def run_code_snippet(wd_path): 54 | # extract the name of the executable 55 | executable = wd_path.split("/")[-1] 56 | 57 | CMD = [f"./test_app"] 58 | 59 | for command in CMD: 60 | click.secho(f"Running: {command} in {wd_path}", fg="blue") 61 | result = subprocess.run( 62 | command, 63 | shell=True, 64 | check=False, # Allow handling of return code manually 65 | cwd=wd_path, 66 | stdout=subprocess.PIPE, 67 | stderr=subprocess.PIPE, 68 | ) 69 | 70 | if result.returncode != 0: 71 | click.secho(f"Error executing: '{command}' in {wd_path}", fg="red") 72 | click.secho(result.stderr.decode(), fg="red") 73 | return False 74 | elif result.stdout: 75 | click.secho(result.stdout.decode(), fg="cyan") 76 | 77 | return True 78 | 79 | 80 | def check_all_code_snippets(): 81 | 82 | # In theory the above commands in CMD could be modified for Windows, but it's probably not worth the effort. 83 | # This check allows the build_book.py script to pass without erroring out when executed on Windows as a band aid fix. 84 | if os.name == "nt": 85 | current_file_path = os.path.abspath(__file__) 86 | click.secho( 87 | f"Not executing {current_file_path} due to lack of Windows support.", 88 | fg="yellow", 89 | ) 90 | return 91 | 92 | for path in CODE_SNIPPET_PATHS: 93 | path = os.path.abspath(path) 94 | if not check_code_snippet(path): 95 | raise RuntimeError(f"Error in running code snippet in {path}") 96 | else: 97 | click.secho(f"Code snippet in {path} is working correctly!", fg="green") 98 | 99 | for path in RUNNABLE_PATHS: 100 | path = os.path.abspath(path) 101 | if not run_code_snippet(path): 102 | raise RuntimeError(f"Error in running code snippet in {path}") 103 | else: 104 | click.secho(f"Run snippet in {path} is working correctly!", fg="green") 105 | 106 | 107 | if __name__ == "__main__": 108 | check_all_code_snippets() 109 | -------------------------------------------------------------------------------- /scripts/e-series.py: -------------------------------------------------------------------------------- 1 | # fmt: off 2 | # Constants for series values for reference 3 | # https://en.wikipedia.org/wiki/E_series_of_preferred_numbers 4 | 5 | E_SERIES = [3, 6, 12, 24, 48, 96, 192] 6 | 7 | E3 = [1.0, 2.2, 4.7] 8 | E6 = [1.0, 1.5, 2.2, 3.3, 4.7, 6.8] 9 | E12 = [1.0, 1.2, 1.5, 1.8, 2.2, 2.7, 3.3, 3.9, 4.7, 5.6, 6.8, 8.2] 10 | E24 = [1.0, 1.1, 1.2, 1.3, 1.5, 1.6, 1.8, 2.0, 2.2, 2.4, 2.7, 3.0, 3.3, 3.6, 3.9, 4.3, 4.7, 5.1, 5.6, 6.2, 6.8, 7.5, 8.2, 9.1] 11 | E48 = [1.00, 1.05, 1.10, 1.15, 1.21, 1.27, 1.33, 1.40, 1.47, 1.54, 1.62, 1.69, 1.78, 1.87, 1.96, 2.05, 2.15, 2.26, 2.37, 2.49, 2.61, 2.74, 2.87, 3.01, 3.16, 3.32, 3.48, 3.65, 3.83, 4.02, 4.22, 4.42, 4.64, 4.87, 5.11, 5.36, 5.62, 5.90, 6.19, 6.49, 6.81, 7.15, 7.50, 7.87, 8.25, 8.66, 9.09, 9.53] 12 | E96 = [1.00, 1.02, 1.05, 1.07, 1.10, 1.13, 1.15, 1.18, 1.21, 1.24, 1.27, 1.30, 1.33, 1.37, 1.40, 1.43, 1.47, 1.50, 1.54, 1.58, 1.62, 1.65, 1.69, 1.74, 1.78, 1.82, 1.87, 1.91, 1.96, 2.00, 2.05, 2.10, 2.15, 2.21, 2.26, 2.32, 2.37, 2.43, 2.49, 2.55, 2.61, 2.67, 2.74, 2.80, 2.87, 2.94, 3.01, 3.09, 3.16, 3.24, 3.32, 3.40, 3.48, 3.57, 3.65, 3.74, 3.83, 3.92, 4.02, 4.12, 4.22, 4.32, 4.42, 4.53, 4.64, 4.75, 4.87, 4.99, 5.11, 5.23, 5.36, 5.49, 5.62, 5.76, 5.90, 6.04, 6.19, 6.34, 6.49, 6.65, 6.81, 6.98, 7.15, 7.32, 7.50, 7.68, 7.87, 8.06, 8.25, 8.45, 8.66, 8.87, 9.09, 9.31, 9.53, 9.76] 13 | E192 = [1.00, 1.01, 1.02, 1.04, 1.05, 1.06, 1.07, 1.09, 1.10, 1.11, 1.13, 1.14, 1.15, 1.17, 1.18, 1.20, 1.21, 1.23, 1.24, 1.26, 1.27, 1.29, 1.30, 1.32, 1.33, 1.35, 1.37, 1.38, 1.40, 1.42, 1.43, 1.45, 1.47, 1.49, 1.50, 1.52, 1.54, 1.56, 1.58, 1.60, 1.62, 1.64, 1.65, 1.67, 1.69, 1.72, 1.74, 1.76, 1.78, 1.80, 1.82, 1.84, 1.87, 1.89, 1.91, 1.93, 1.96, 1.98, 2.00, 2.03, 2.05, 2.08, 2.10, 2.13, 2.15, 2.18, 2.21, 2.23, 2.26, 2.29, 2.32, 2.34, 2.37, 2.40, 2.43, 2.46, 2.49, 2.52, 2.55, 2.58, 2.61, 2.64, 2.67, 2.71, 2.74, 2.77, 2.80, 2.84, 2.87, 2.91, 2.94, 2.98, 3.01, 3.05, 3.09, 3.12, 3.16, 3.20, 3.24, 3.28, 3.32, 3.36, 3.40, 3.44, 3.48, 3.52, 3.57, 3.61, 3.65, 3.70, 3.74, 3.79, 3.83, 3.88, 3.92, 3.97, 4.02, 4.07, 4.12, 4.17, 4.22, 4.27, 4.32, 4.37, 4.42, 4.48, 4.53, 4.59, 4.64, 4.70, 4.75, 4.81, 4.87, 4.93, 4.99, 5.05, 5.11, 5.17, 5.23, 5.30, 5.36, 5.42, 5.49, 5.56, 5.62, 5.69, 5.76, 5.83, 5.90, 5.97, 6.04, 6.12, 6.19, 6.26, 6.34, 6.42, 6.49, 6.57, 6.65, 6.73, 6.81, 6.90, 6.98, 7.06, 7.15, 7.23, 7.32, 7.41, 7.50, 7.59, 7.68, 7.77, 7.87, 7.96, 8.06, 8.16, 8.25, 8.35, 8.45, 8.56, 8.66, 8.76, 8.87, 8.98, 9.09, 9.20, 9.31, 9.42, 9.53, 9.65, 9.76, 9.88] 14 | 15 | E_VALUES_LIST = [E3, E6, E12, E24, E48, E96, E192] 16 | assert len(E_VALUES_LIST) == len(E_SERIES) 17 | 18 | E_VALUES_DICT = {} 19 | for i, e_series in enumerate(E_SERIES) : # series is singular and plural which is a bit confusing but hopefully this is self-explanatory lol 20 | E_VALUES_DICT[f"E{e_series}"] = E_VALUES_LIST[i] 21 | assert e_series == len(E_VALUES_LIST[i]) 22 | 23 | # fmt: on 24 | -------------------------------------------------------------------------------- /scripts/format.py: -------------------------------------------------------------------------------- 1 | from util import * 2 | import subprocess 3 | import sys 4 | import argparse 5 | 6 | EXCLUDE_DIRS = [".vscode", "build", "libs", "fw-hw-questions-venv", "output"] 7 | EMBEDDED_EXTENSIONS = (".c", ".h", ".cpp", ".hpp") 8 | PYTHON_EXTENSION = ".py" 9 | 10 | 11 | def embedded_fmt(dry_run=False): 12 | """ 13 | Format all C/C++ files at once using clang-format. 14 | If dry_run is True, it will only check if any formatting is required without making changes. 15 | """ 16 | embedded_files = get_files_with_extensions(EMBEDDED_EXTENSIONS, EXCLUDE_DIRS) 17 | if embedded_files: 18 | if dry_run: 19 | result = subprocess.run( 20 | ["clang-format", "--dry-run", "--Werror", *embedded_files], 21 | capture_output=True, 22 | text=True, 23 | ) 24 | if result.returncode != 0: 25 | print("C/C++ files need formatting! Specific issues:") 26 | diff_result = subprocess.run( 27 | ["clang-format", "--verbose", *embedded_files], 28 | capture_output=True, 29 | text=True, 30 | ) 31 | print(diff_result.stdout) 32 | sys.exit(1) 33 | 34 | else: 35 | subprocess.run(["clang-format", "-i", *embedded_files]) 36 | 37 | 38 | def python_fmt(dry_run=False): 39 | """ 40 | Format all Python files at once using black. 41 | If dry_run is True, it will only check if any formatting is required without making changes. 42 | """ 43 | python_files = get_files_with_extensions((PYTHON_EXTENSION,), EXCLUDE_DIRS) 44 | if python_files: 45 | if dry_run: 46 | result = subprocess.run( 47 | ["black", "--check", "--diff", *python_files], 48 | capture_output=True, 49 | text=True, 50 | ) 51 | if result.returncode != 0: 52 | print("Python formatter failed! Specific issues:") 53 | print(result.stdout) # Output the diff of formatting issues 54 | sys.exit(1) 55 | else: 56 | subprocess.run(["black", *python_files]) 57 | 58 | 59 | def main(): 60 | # Set up argument parsing 61 | parser = argparse.ArgumentParser( 62 | description="Format source files with specified tools." 63 | ) 64 | parser.add_argument( 65 | "--dry-run", 66 | action="store_true", 67 | help="Check for needed formatting without applying changes.", 68 | ) 69 | 70 | # Parse arguments 71 | args = parser.parse_args() 72 | 73 | # Run the formatting with or without dry-run 74 | embedded_fmt(dry_run=args.dry_run) 75 | python_fmt(dry_run=args.dry_run) 76 | 77 | 78 | if __name__ == "__main__": 79 | main() 80 | -------------------------------------------------------------------------------- /scripts/python_requirements.txt: -------------------------------------------------------------------------------- 1 | black 2 | matplotlib 3 | click 4 | numpy 5 | cairosvg 6 | pillow 7 | control 8 | -------------------------------------------------------------------------------- /scripts/setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | import subprocess 3 | import argparse 4 | import json 5 | 6 | 7 | def run_command(command): 8 | """Run a system command and handle errors if any.""" 9 | try: 10 | result = subprocess.run(command, shell=True, check=True) 11 | if result.returncode != 0: 12 | print(f"Command failed: {command}") 13 | except subprocess.CalledProcessError as e: 14 | print(f"An error occurred: {e}") 15 | exit(1) 16 | 17 | 18 | def update_and_upgrade(skip_upgrade=False, no_sudo=False): 19 | """Update and optionally upgrade the system's package list.""" 20 | print("Updating apt package list...") 21 | COMMANDS = [ 22 | "apt-get update", 23 | ] 24 | 25 | if not skip_upgrade: 26 | print("Upgrading apt packages...") 27 | COMMANDS.append("apt-get upgrade -y") 28 | 29 | for command in COMMANDS: 30 | if no_sudo: 31 | run_command(command) 32 | else: 33 | run_command(f"sudo {command}") 34 | 35 | 36 | def install_apt_components(components, no_sudo=False): 37 | """Install components using apt-get.""" 38 | print("Installing apt components...") 39 | component_list = " ".join(components) 40 | command = f"apt-get install -y {component_list}" 41 | 42 | if no_sudo: 43 | run_command(command) 44 | else: 45 | run_command(f"sudo {command}") 46 | 47 | 48 | def install_pip_components(components): 49 | """Install components using pip.""" 50 | if components is not None: 51 | print("Installing pip components...") 52 | component_list = " ".join(components) 53 | run_command(f"pip3 install {component_list}") 54 | else: 55 | print("No pip components to install.") 56 | 57 | 58 | def update_submodules(): 59 | """Update git submodules.""" 60 | print("Updating git submodules...") 61 | run_command("git submodule update --init --recursive") 62 | 63 | 64 | def read_json_file(json_path): 65 | """Read the build requirements from a JSON file.""" 66 | if not os.path.exists(json_path): 67 | raise FileNotFoundError(f"Error: The file {json_path} does not exist.") 68 | else: 69 | with open(json_path, "r") as f: 70 | return json.load(f) 71 | 72 | 73 | def read_requirements_txt(txt_path): 74 | """Read the pip requirements from a requirements.txt file.""" 75 | if not os.path.exists(txt_path): 76 | raise FileNotFoundError(f"Error: The file {txt_path} does not exist.") 77 | else: 78 | with open(txt_path, "r") as f: 79 | return [line.strip() for line in f if line.strip()] 80 | 81 | 82 | if __name__ == "__main__": 83 | # Argument parser to handle skip_upgrade option 84 | parser = argparse.ArgumentParser(description="Install necessary development tools.") 85 | parser.add_argument( 86 | "--skip-upgrade", 87 | action="store_true", 88 | help="Skip the apt upgrade step", 89 | ) 90 | parser.add_argument( 91 | "--json", 92 | default="scripts/build_requirements.json", 93 | help="Path to the JSON file with apt build requirements", 94 | ) 95 | parser.add_argument( 96 | "--requirements", 97 | default="scripts/python_requirements.txt", 98 | help="Path to the pip requirements.txt file", 99 | ) 100 | parser.add_argument( 101 | "--no-sudo", 102 | action="store_true", 103 | help="Do not use sudo to install packages", 104 | ) 105 | args = parser.parse_args() 106 | 107 | # Read from JSON and requirements.txt 108 | try: 109 | build_requirements = read_json_file(args.json) 110 | apt_components = build_requirements.get("apt", []) 111 | except FileNotFoundError as e: 112 | print(e) 113 | exit(1) 114 | 115 | try: 116 | pip_components = read_requirements_txt(args.requirements) 117 | except FileNotFoundError as e: 118 | print(e) 119 | exit(1) 120 | 121 | # Execute functions 122 | update_and_upgrade(skip_upgrade=args.skip_upgrade, no_sudo=args.no_sudo) 123 | install_apt_components(apt_components, no_sudo=args.no_sudo) 124 | install_pip_components(pip_components) 125 | update_submodules() 126 | -------------------------------------------------------------------------------- /scripts/util.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | 4 | def get_files_with_extensions(extensions, exclude_dirs, base_path="."): 5 | """ 6 | Get a list of all files with the specified extensions, excluding certain directories. 7 | """ 8 | files = [] 9 | for root, dirs, filenames in os.walk(base_path): 10 | # Modify 'dirs' in-place to exclude unwanted directories by name 11 | dirs[:] = [d for d in dirs if d not in exclude_dirs] 12 | files.extend( 13 | os.path.join(root, filename) 14 | for filename in filenames 15 | if filename.endswith(extensions) 16 | ) 17 | return files 18 | -------------------------------------------------------------------------------- /src/64-bit-timer.tex: -------------------------------------------------------------------------------- 1 | \documentclass[main.tex]{subfiles} 2 | \begin{document} 3 | 4 | \section{Given a 64-bit timer consisting of two 32-bit count registers, implement a function to get the 64 bit count.} 5 | Assume that the timer is initialized, counts up, and updates itself atomically. The following registers are available, with the header for the question available in Listing~\ref{code:timer-registers}: 6 | \begin{itemize} 7 | \item \texttt{CNT\_LOW} is the lower 32 bits of the timer count. 8 | \item \texttt{CNT\_HIGH} is the upper 32 bits of the timer. 9 | \end{itemize} 10 | 11 | \lstinputlisting[caption={Timer Registers}, label={code:timer-registers}]{code/timer/timer_registers.h} 12 | 13 | \spoilerline 14 | 15 | \subsection{Timer Background} 16 | As a review, a timer is a hardware peripheral that can be configured in firmware to perform specific operations. Generally, timers have a counter that increments or decrements at a fixed rate, and the current count of the timer is accessed by means of a dedicated \texttt{COUNT} register. In the context of this question, it's mentioned that the timer counts up, meaning the count value increases with time at some unspecified rate. As an example, suppose we want to keep track of the number of milliseconds that have passed since the timer was started. If the timer is configured to increment every millisecond, the count value will increase by 1 every millisecond, and the count value can be read to determine the time elapsed in milliseconds. 17 | 18 | \subsection{Simple Implementation} 19 | On first glance, it's tempting to claim the answer is as simple as combining the two 32-bit registers into a 64-bit value, much like what is presented in Listing~\ref{code:timer-64bit-naive}. 20 | 21 | \lstinputlisting[caption={Naive 64-bit Timer Read}, label={code:timer-64bit-naive}]{code/timer/timer_64bit_naive.c} 22 | 23 | \noindent However, this implementation fails to consider that the 32-bit registers are updated by timer hardware asynchronously. If the timer updates the \texttt{CNT\_HIGH} register between reading \texttt{CNT\_LOW} and \texttt{CNT\_HIGH}, the resulting 64-bit value will be incorrect. Consider the following scenario: 24 | \begin{enumerate} 25 | \item Assume that at the start of the function, the timer count register values are as follows: \texttt{CNT\_LOW = 0xFFFFFFFF}, while \texttt{CNT\_HIGH = 0x00000000}. 26 | \item The function reads the \texttt{CNT\_LOW} value as \texttt{0xFFFFFFFF}.\footnote{This is $2^{32}-1$ in hex - \texttt{0x} means hexadecimal notation in C and is commonly used in datasheets and embedded software manuals as a result of the industry's adoption of C and C++.} 27 | \item In between reading \texttt{CNT\_LOW} and \texttt{CNT\_HIGH}, the timer increments. This causes \texttt{CNT\_LOW} to wrap around to \texttt{0x00000000} and consequently \texttt{CNT\_HIGH} to increment to \texttt{0x00000001}. 28 | \item The function now reads the \texttt{CNT\_HIGH} value as \texttt{0x00000001}. 29 | \item The resulting 64-bit value is \texttt{0x00000001 FFFFFFFF}, which is incorrect. The correct value should be \texttt{0x00000001 00000000}. 30 | \end{enumerate} 31 | 32 | \noindent The implications of this is that the 64 bit time value is no longer \textit{monotonic} (constantly increasing), aside from being straight up incorrect. Consumers of this function may experience unexpected behavior if the timer is used for timekeeping or scheduling, as the time value may appear to jump backwards if called near a timer update/roll-over. Note that this issue would also occur if the read order of the high and low COUNT registers were swapped in the implementation. 33 | 34 | \subsection{Correct Implementation} 35 | To correctly read the 64-bit timer value, we need to ensure that the \texttt{CNT\_HIGH} register and \texttt{CNT\_LOW} register are 'synchronized' in time. This can be achieved by reading the \texttt{CNT\_HIGH} register first, then reading the \texttt{CNT\_LOW} register, and finally reading the \texttt{CNT\_HIGH} register again. If the two \texttt{CNT\_HIGH} reads are equal, we can be confident that the 64-bit value is correct as a roll-over did not occur. If a roll-over did occur, we re-read the low and high values and use that for the computation of the 64 bit time, as we are no longer close to a roll-over event. The correct implementation is shown in Listing~\ref{code:timer-64bit-correct}. 36 | 37 | \lstinputlisting[caption={Correct 64-bit Timer Read}, label={code:timer-64bit-correct}]{code/timer/timer_64bit_correct.c} 38 | 39 | \end{document} 40 | -------------------------------------------------------------------------------- /src/adc_init.tex: -------------------------------------------------------------------------------- 1 | \documentclass[main.tex]{subfiles} 2 | 3 | \begin{document} 4 | 5 | \section{Given the following datasheet and code snippet, initialize the ADC and write a polling function to read the ADC voltage.} 6 | 7 | \subsection{Supporting Problem Information} 8 | Assume that the ADC is to be run with an ADC conversion clock frequency of 1 MHz. Implement the functions shown in Listing \ref{code:adc-init-user-function} to initialize the ADC and read the ADC voltage. 9 | 10 | \lstinputlisting[caption={User Implemented Functions}, label={code:adc-init-user-function}]{code/adc_init/adc_init_header.h} 11 | 12 | \subsubsection{Sample Datasheet} 13 | The MythicalMicrocontroller is a microcontroller with a clock frequency of 8 MHz. It features a single-shot 12-bit analog-to-digital converter (ADC) with a reference voltage of 3.3 V, with a multiplexer to select between 4 input channels. 14 | 15 | \paragraph{ADC Configuration Register (WRITE) - ADDR 0x4000007C} 16 | The ADC configuration register is used to configure the ADC module with the key system parameters. The ADC must be enabled and ready before any conversions can be performed. 17 | \begin{table}[H] 18 | \centering 19 | \begin{tabular}{|l|c|p{10cm}|} 20 | \hline 21 | \textbf{Name} & \textbf{Bits} & \textbf{Description} \\ \hline 22 | \texttt{ADC\_EN} & 7 & ADC Enable Bit - Write \texttt{1} to enable the ADC for conversion. \\ \hline 23 | \texttt{ADC\_CONVERSION\_BEGIN} & 6 & Set bit to begin an ADC conversion. The \texttt{ADC\_INITIALIZED} bit in the \texttt{ADC\_STATUS\_REGISTER} must be asserted before this bit is set. This bit clears automatically once a conversion finishes. \\ \hline 24 | \texttt{ADC\_MUX\_SEL 1:0} & 5:4 & ADC multiplexer channel selection bits. \\ \hline 25 | \texttt{ADC\_CLK\_DIV 3:0} & 3:0 & ADC Clock prescaler. The ADC clock frequency is determined by the following equation:\newline$f_{ADC\_CLK} = f_{MCU}/(ADC\_CLK\_DIV + 1)$ \\ \hline 26 | \end{tabular} 27 | \caption{ADC Register Description} 28 | \label{tab:adc_en_register} 29 | \end{table} 30 | 31 | \paragraph{ADC Status Register (READ) - ADDR 0x4000007D} 32 | The ADC status register is used to check the status of the ADC module. The ADC is ready to perform a conversion when the \texttt{ADC\_INITIALIZED} bit is set. The \texttt{ADC\_BUSY} bit is set when the ADC is performing a conversion, and cleared when the conversion is complete. 33 | \begin{table}[H] 34 | \centering 35 | \begin{tabular}{|l|c|p{10cm}|} 36 | \hline 37 | \textbf{Name} & \textbf{Bits} & \textbf{Description} \\ \hline 38 | \texttt{ADC\_INITIALIZED} & 7 & ADC is enabled and ready for use when bit is \texttt{1} \\ \hline 39 | \texttt{RESERVED} & 6:1 & Reserved \\ \hline 40 | \texttt{ADC\_BUSY} & 0 & ADC is busy with processing the conversion when the bit is asserted. A transition from the bit being asserted to being deasserted indicates the completion of a conversion. \\ \hline 41 | \end{tabular} 42 | \caption{ADC Register Description} 43 | \label{tab:adc_status_register} 44 | \end{table} 45 | 46 | \paragraph{ADC Data HIGH Register (READ) - ADDR 0x4000007E} 47 | The ADC Data H register contains the upper 4 bits of the 12-bit ADC conversion result. 48 | \begin{table}[H] 49 | \centering 50 | \begin{tabular}{|l|c|p{10cm}|} 51 | \hline 52 | \textbf{Name} & \textbf{Bits} & \textbf{Description} \\ \hline 53 | \texttt{RESERVED} & 7:4 & Reserved \\ \hline 54 | \texttt{ADC\_DATA\_H 3:0} & 3:0 & Upper 4 bits of the 12-bit ADC conversion result \\ \hline 55 | \end{tabular} 56 | \caption{ADC Data H Register Description} 57 | \label{tab:adc_data_h_register} 58 | \end{table} 59 | 60 | \paragraph{ADC Data LOW Register (READ) - ADDR 0x4000007F} 61 | The ADC Data L register contains the lower 8 bits of the 12-bit ADC conversion result. 62 | \begin{table}[H] 63 | \centering 64 | \begin{tabular}{|l|c|p{10cm}|} 65 | \hline 66 | \textbf{Name} & \textbf{Bits} & \textbf{Description} \\ \hline 67 | \texttt{ADC\_DATA\_L 7:0} & 7:0 & Lower 8 bits of the 12-bit ADC conversion result \\ \hline 68 | \end{tabular} 69 | \caption{ADC Data L Register Description} 70 | \label{tab:adc_data_l_register} 71 | \end{table} 72 | 73 | \spoilerline 74 | 75 | \subsection{Setting Up the Problem} 76 | When coming across a peripheral initialization problem, it is essential to break down the problem into smaller parts and to understand the requirements - they often have a lot of bark, but not much bite given the limited scope of an interview. The problem can be broken down into two main parts: initialization and conversion. As a first step, we can go ahead and define the register addresses and bit positions for the ADC configuration and status registers, shown in Listing \ref{code:adc-registers}. 77 | 78 | \lstinputlisting[caption={ADC Registers and Bit Positions}, label={code:adc-registers}]{code/adc_init/adc_init_registers.h} 79 | \noindent Note the use of \texttt{volatile} in the register definitions. This keyword tells the compiler that the value of the variable can change at any time, which is essential for memory-mapped registers. 80 | 81 | \subsection {Initialization} 82 | Initializing the ADC can be broken down into the following steps. The code is shown in Listing \ref{code:adc-init}. 83 | \begin{enumerate} 84 | \item \textbf{Set the ADC Clock Frequency}: Calculate the ADC clock prescaler value to achieve a 1 MHz ADC conversion clock frequency given the 8 MHz MCU clock frequency. We can use the formula $f_{ADC\_CLK} = f_{MCU}/(ADC\_CLK\_DIV + 1)$ to calculate the prescaler ($ADC\_CLK\_DIV$) value (7). 85 | \item \textbf{Enable the ADC}: Write to the ADC configuration register to enable the ADC. 86 | \item \textbf{Poll the ADC Status Register}: Check if the ADC is ready for a conversion. 87 | \end{enumerate} 88 | 89 | \lstinputlisting[caption={ADC Initialization}, label={code:adc-init}]{code/adc_init/adc_init.c} 90 | 91 | \subsection{Conversion} 92 | The conversion problem can be addressed as follows. The code is shown in Listing \ref{code:adc-conversion}. 93 | \begin{enumerate} 94 | \item \textbf{Set the ADC Multiplexer Channel}: Write to the ADC configuration register to select the desired ADC multiplexer channel. 95 | \item \textbf{Begin the ADC Conversion}: Write to the ADC configuration register to begin an ADC conversion. 96 | \item \textbf{Poll the ADC Status Register}: Wait for the ADC to finish the conversion by polling the ADC status register. 97 | \item \textbf{Read the ADC Data Registers}: Read the ADC data registers to get the 12-bit ADC conversion result. We need to read the ADC data registers in two separate reads (high and low) and combine the results by shifting the high bits left by 8 and ORing with the low bits. 98 | \item \textbf{Calculate the ADC Voltage}: Calculate the ADC voltage from the 12-bit ADC conversion result by using the formula $V_{ADC} = \frac{ADC\_RESULT \cdot V_{REF}}{2^{12} - 1}$. Note that the $2^{12} - 1$ term is the maximum value of a 12-bit number, and corrosponds to the ADC reference voltage. 99 | \end{enumerate} 100 | 101 | \lstinputlisting[caption={ADC Conversion}, label={code:adc-conversion}]{code/adc_init/adc_conversion.c} 102 | 103 | \subsection{Follow-ups} 104 | \begin{itemize} 105 | \item \textbf{Avoiding Polling}: What are strategies for avoiding polling in the ADC conversion function (ex: DMA, interrupts, etc)? 106 | \item \textbf{Sample Frequency}: Generally, what is the impact of the ADC clock frequency on the ADC conversion time and resolution? 107 | \item \textbf{ADC Error Sources}: What are sources of error in an ADC and how are they mitigated? 108 | \end{itemize} 109 | 110 | \end{document} 111 | -------------------------------------------------------------------------------- /src/appendix_more _opamps.tex: -------------------------------------------------------------------------------- 1 | \documentclass[main.tex]{subfiles} 2 | \begin{document} 3 | 4 | \section{Extra Practice: Solve the transfer function of the following circuits.} \label{extra_practice:more_opamps} 5 | 6 | \noindent The following circuits are similar to those in Section \ref{section:opamp} and are given as extra excersices with final answers only for checking. 7 | 8 | \begin{figure}[H] 9 | \begin{center} 10 | \begin{minipage}{0.45\textwidth} 11 | \centering 12 | \begin{circuitikz}[american] 13 | \draw (0,0) node[left]{$V_{in}$} to[short] ++ (1,0) 14 | node[op amp, noinv input up, anchor=+](op-amp){} 15 | (op-amp.-) -- ++(0,-1) coordinate(FB) 16 | (FB) to[short] (FB -| op-amp.out) -- (op-amp.out) 17 | to [short] ++(1,0) node[right]{$V_{out}$}; 18 | \end{circuitikz} 19 | \caption{Circuit A} 20 | \label{fig:unity-amp} 21 | \end{minipage}% 22 | \hfill% 23 | \begin{minipage}{0.45\textwidth} 24 | \centering 25 | \begin{circuitikz}[american] 26 | \draw (0, 0) node[left]{$V_{in}$}; 27 | \draw (0, 0) to[resistor, l=$R_i$] (2, 0); 28 | \draw (2, 0) node[op amp, anchor=-](op-amp){}; 29 | \draw (op-amp.+) node[ground]{}; 30 | \draw (2, 0) -- (2, 1); 31 | \draw (2, 1) to[resistor, l=$R_f$] (4.5, 1); 32 | \draw (4.5, 1) -- (4.5, -0.5); 33 | \draw (op-amp.out) to [short] ++ (0.5, 0) node[right]{$V_{out}$}; 34 | \end{circuitikz} 35 | \caption{Circuit B} 36 | \label{fig:inverting_amp} 37 | \end{minipage} 38 | \end{center} 39 | \end{figure} 40 | \begin{figure}[H] 41 | \begin{center} 42 | \begin{minipage}{0.45\textwidth} 43 | \centering 44 | \begin{circuitikz}[american, transform shape] 45 | \draw (0,0) node[left]{$V_{in}$} to[short] ++ (1,0) 46 | node[op amp, noinv input up, anchor=+](op-amp){} 47 | (op-amp.-) -- ++(0,-1) coordinate(FB) 48 | to[R=$R_i$] ++(0,-2) node[ground]{} 49 | (FB) to[R=$R_f$] (FB -| op-amp.out) -- (op-amp.out) 50 | to [short] ++(1,0) node[right]{$V_{out}$}; 51 | \end{circuitikz} 52 | \caption{Circuit C} 53 | \label{fig:non_inverting_amp} 54 | \end{minipage}% 55 | \hfill% 56 | \begin{minipage}{0.45\textwidth} 57 | \centering 58 | \begin{circuitikz}[american] 59 | \draw (0, 0) node[left]{$V_{in-}$}; 60 | \draw (0, 0) to[resistor, l=$R_1$, i=$I_{1}$] (2, 0); 61 | \draw (2, 0) node[op amp, anchor=-](op-amp){}; 62 | \draw (2, 0) -- (2, 1); 63 | \draw (2, 1) to[resistor, l=$R_3$] (4.5, 1); 64 | \draw (4.5, 1) -- (4.5, -0.5); 65 | \draw (op-amp.out) to [short] ++ (0.5, 0) node[right]{$V_{out}$}; 66 | \draw (0, -1) node[left]{$V_{in+}$}; 67 | \draw (0, -1) to[resistor, l=$R_2$, i=$I_{2}$] (2, -1); 68 | \draw (2, -1) to[resistor, l=$R_4$] (2, -3); 69 | \draw (2, -3) node[ground]{}; 70 | \end{circuitikz} 71 | \caption{Circuit D} 72 | \label{fig:difference_amp} 73 | \end{minipage} 74 | \end{center} 75 | \end{figure} 76 | 77 | \spoilerline 78 | 79 | \subsection{Solutions} 80 | \begin{itemize} 81 | \item Circuit A is a unity gain amplifier where $\frac{V_{out}}{V_{in}} = 1$. 82 | \item Circuit B is an inverting amplifier where $\frac{V_{out}}{V_{in}} = -\frac{R_f}{R_i}$. 83 | \item Circuit C is a non-inverting amplifier where $\frac{V_{out}}{V_{in}} = 1 + \frac{R_f}{R_i}$. 84 | \item Circuit D is a differential amplifier where $V_{out} = -V_{in-} \cdot \frac{R_3}{R_1} + V_{in+} \cdot \frac{R_4}{R_2 + R_4} \cdot \frac{R_1 + R_3}{R_1}$. Consider a special case when $R_1 = R_2$ and $R_3 = R_4$ where the transfer function becomes $\frac{V_{out}}{V_{in+} - V_{in-}} = \frac{R_3}{R_1}$. Additionally, if $R_1 = R_2 = R_3 = R_4$ then $V_{out} = V_{in+} - V_{in-}$. 85 | \end{itemize} 86 | 87 | \end{document} 88 | -------------------------------------------------------------------------------- /src/appendix_more_passives.tex: -------------------------------------------------------------------------------- 1 | \documentclass[main.tex]{subfiles} 2 | \begin{document} 3 | 4 | \section{Extra Practice: Determine the step response of the following circuits.} \label{extra_practice:more_passives} 5 | 6 | \noindent State any assumptions regarding component values. These questions are more complex then those given in Section \ref{section:passives}, but are fun to consider. 7 | 8 | \begin{figure}[H] 9 | \begin{center} 10 | \begin{minipage}{0.45\textwidth} 11 | \centering 12 | \begin{circuitikz}[american] 13 | \draw (0,0) to[isource, l=$I_{in}$] (0,3) -- (2,3) to[C=$C$] (2,0) -- (0,0); 14 | \draw (2, 3) node[right] {$V_{out}$}; 15 | \draw (1, 0) node[ground]{}; 16 | \label{fig:c_current_source} 17 | \end{circuitikz} 18 | \caption{Circuit E} 19 | \end{minipage}% 20 | \hfill% 21 | \begin{minipage}{0.45\textwidth} 22 | \centering 23 | \begin{circuitikz}[american] 24 | \draw (0,0) to[isource, l=$I_{in}$] (0,3); 25 | \draw (0, 3) to[resistor, l=$R$] (3, 3); 26 | \draw (3, 3) node[right] {$V_{out}$}; 27 | \draw (3, 3) to[capacitor, l=$C$] (3, 0); 28 | \draw (3, 0) -- (0, 0); 29 | \draw(1.5, 0) node[ground]{}; 30 | \label{fig:rc_current_source} 31 | \end{circuitikz} 32 | \caption{Circuit F} 33 | \end{minipage} 34 | \end{center} 35 | \end{figure} 36 | \begin{figure}[H] 37 | \begin{center} 38 | \begin{minipage}{0.45\textwidth} 39 | \centering 40 | \begin{circuitikz}[american] 41 | \draw (0, 4) node[anchor=east] {$V_{in}$}; 42 | \draw (0, 4) to[inductor, l=$L$] (5, 4); 43 | \draw (3.5, 4) to[capacitor, l=$C$] (3.5, 2); 44 | \draw (5, 4) node[right] {$V_{out}$}; 45 | \draw (-0.5, 2) -- (5.5, 2); 46 | \draw (3, 2) node[ground]{}; 47 | \label{fig:lseries_cshunt} 48 | \end{circuitikz} 49 | \caption{Circuit G} 50 | \end{minipage}% 51 | \hfill% 52 | \begin{minipage}{0.45\textwidth} 53 | \centering 54 | \begin{circuitikz}[american] 55 | \draw (0, 4) node[anchor=east] {$V_{in}$}; 56 | \draw (0, 4) -- (0.75, 4); 57 | \draw (0.75, 4) to[inductor, l=$L$] (2.5, 4); 58 | \draw (2.5, 4) to [capacitor, l=$C$] (4.25, 4); 59 | \draw (4.25, 4) -- (5, 4); 60 | \draw (5, 4) node[right] {$V_{out}$}; 61 | \draw (-0.5, 2) -- (5.5, 2); 62 | \draw (3, 2) node[ground]{}; 63 | \label{fig:lseries_cseries} 64 | \end{circuitikz} 65 | \caption{Circuit H} 66 | \end{minipage} 67 | \end{center} 68 | \end{figure} 69 | % More circuit ideas will be added in the second edition, see notes gdocs 70 | 71 | \spoilerline 72 | 73 | \subsection{Circuit E: Series Capacitor with Current Source} 74 | 75 | Recall that for a capacitor, $I = C \cdot \dfrac{dV}{dt}$ and current through series elements is identical. This results in the solution given in Figure \ref{fig:step-response-series-cap-with-current-source}. 76 | 77 | \begin{figure}[H] 78 | \centering 79 | \includegraphics[width=0.8\textwidth]{generated_images/series_cap_with_current_source.png} 80 | \caption{Step Response of a Circuit with a Series Capacitor and Current Source} 81 | \label{fig:step-response-series-cap-with-current-source} 82 | \end{figure} 83 | 84 | \subsection{Circuit F: Series RC with Current Source} 85 | As current through series elements is identical and for a resistor, $V = I \cdot R$, adding the series resistor does not affect the step response of this circuit. Circuit E and Circuit F have identical step response plots. 86 | 87 | \subsection{Circuit G: Series Inductor with Shunt Capacitor} 88 | This circuit results in LC resonance and can be found inside a buck converter circuit. The step response solution is given in Figure \ref{fig:step-response-series-ind-with-shunt-cap}. 89 | 90 | \begin{figure}[H] 91 | \centering 92 | \includegraphics[width=0.8\textwidth]{generated_images/series_inductor_with_shunt_cap.png} 93 | \caption{Step Response of a Circuit with a Series Inductor with Shunt Capacitor} 94 | \label{fig:step-response-series-ind-with-shunt-cap} 95 | \end{figure} 96 | 97 | \subsection{Circuit H: Series Inductor and Capacitor} 98 | This circuit does not result in resonance as there is no current path for the two series elements. The step response solution is $V_{out} = V_{in}$. 99 | % Very vague for a kind of troll question but yea. 100 | 101 | \end{document} 102 | -------------------------------------------------------------------------------- /src/appendix_packet_parsing_tests.tex: -------------------------------------------------------------------------------- 1 | \documentclass[main.tex]{subfiles} 2 | \begin{document} 3 | 4 | \section{Extra Practice: Write a unit test for a packet parsing function.} \label{extra_practice:packet_parsing_tests} 5 | 6 | \spoilerline 7 | 8 | \subsection{Solution} 9 | \noindent As mentioned in Question \ref{section:packet_parsing} (\textit{\nameref{section:packet_parsing}}), unit tests are a crucial part of software development. They help ensure that the code behaves as expected and catches bugs early in the development process. In this section, an example of a unit test framework and unit tests for the packet parsing function are presented. Note that the tests and framework are crude to demonstrate the type of code that would be written in an interview setting. In real-life, a dedicated test framework, like CppUTest or GoogleTest, is strongly recommended. \newline 10 | 11 | \newnoindentpara The unit tests in principle work by the expected output being the extracted values as well as the return value of the parsing function. Listing \ref{code:parsing_test_function} shows example unit tests and crude test framework for testing the packet parsing function. 12 | 13 | \lstinputlisting[caption={Parsing Test Function}, label={code:parsing_test_function}]{code/packet_parsing/main.c} 14 | 15 | \end{document} 16 | -------------------------------------------------------------------------------- /src/appendix_switched_divider.tex: -------------------------------------------------------------------------------- 1 | \documentclass[main.tex]{subfiles} 2 | \begin{document} 3 | 4 | \section{Extra Practice: Propose a simple circuit to disable current consumption when the voltage divider is not needed.} \label{extra_practice:switched_divider} 5 | 6 | \spoilerline 7 | 8 | % This is a really important concept for low power systems but is fairly complex 9 | \subsection{Quiescent Current} 10 | Quiescent current is current drawn when a circuit is inactive or not being used and is proportional to power consumption. Reducing the quiescent current of a voltage divider can be done by increasing $R_{sum}$, however, this only gets you so far. For low power devices, transistor based circuits can be used to disable current flow when sampling the voltage divider is not necessary. An example circuit is given in Figure \ref{fig:voltage_divider_switched}. 11 | \begin{figure}[H] 12 | \begin{center} 13 | \begin{circuitikz}[american] 14 | \draw(-1, 3.5) node[pigfete, bodydiode, rotate=90](q1){}; 15 | \draw (-4, 3.5) -- (q1.S); 16 | \draw (-4, 3.5) node[anchor=east] {$V_{in}$}; 17 | \draw (q1.G) ++ (0, -1.75) node[nigfete, bodydiode](q2){}; 18 | \draw (-3, 3.5) to[resistor, l=$R_{pullup}$] (-3, 1.5); 19 | \draw (-3, 1.5) -- (q2.D); 20 | \draw (q2.S) node[ground]{}; 21 | \draw (q2.D) -- (q1.G); 22 | \draw (-4, 0.5) node[anchor=east] {$V_{ctrl}$}; 23 | \draw (-4, 0.5) -- (q2.G); 24 | \draw (-0.5, 3.5) -- (0.5, 3.5) -- (0.5, 3); 25 | \draw (0.5, 3) to[resistor, l=$R_t$] (0.5, 1.5) to[resistor, l=$R_b$] (0.5, 0); 26 | \draw (0.5, 0) node[ground]{}; 27 | \draw (0.5, 1.5) -- (1.25, 1.5) node[right] {$V_{out}$}; 28 | \end{circuitikz} 29 | \caption{Switched Voltage Divider Circuit} 30 | \label{fig:voltage_divider_switched} 31 | \end{center} 32 | \end{figure} 33 | % comment, This circuit is also explained in https://electronics.stackexchange.com/questions/64490/low-current-battery-monitoring/64491#64491 & https://discord.com/channels/776618956638388305/779145203688013845/1199193108336889958 . It's fairly interesting so I figure it's worth including in the guide though it is a bit scope creep. In reality dual FETs in a single package can often be found for cheap so an NPN isn't usually used, thats why i drew it this way. 34 | 35 | \end{document} 36 | -------------------------------------------------------------------------------- /src/bitstream_parity.tex: -------------------------------------------------------------------------------- 1 | \documentclass[main.tex]{subfiles} 2 | \begin{document} 3 | 4 | \section{Implement a function to validate the parity of a bitstream.} 5 | Assume the following header in Listing \ref{code:bitstream-parity-header} is available for use in the bitstream parity implementation. 6 | 7 | \lstinputlisting[caption={Bitstream Parity Header}, label={code:bitstream-parity-header}]{code/bitstream_parity/bitstream_parity.h} 8 | 9 | \spoilerline 10 | 11 | \subsection{Concept of Parity} 12 | In digital communication, a parity bit is an extra bit appended to a binary data stream to assist in error detection. The goal is to keep the number of 1's in the data stream (including the parity bit) either even (\textit{even parity}) or odd (\textit{odd parity}), depending on the parity scheme. The \textit{parity} of a binary number refers to whether the number of 1's in the binary representation of the number is even or odd. Figure \ref{fig:parity-waveform-diagram} shows waveforms of digital signals with no parity, odd, and even parity, as well as even parity with an error. 13 | 14 | \begin{figure}[H] 15 | \centering 16 | \includegraphics[scale=0.4]{generated_images/svg_generated/parity.png} 17 | \caption{Parity Waveform Diagram} 18 | \label{fig:parity-waveform-diagram} 19 | \end{figure} 20 | 21 | \subsection {Counting number of ones in a Byte} 22 | To implement a parity bit, we need to count the number of 1's in the data stream.\footnote{It's not uncommon to be asked this as a standalone question.} 23 | \subsubsection{Bitwise Operations} 24 | Before counting the number of \texttt{1}s in a byte, it’s important to understand bitwise operations. If you’re not familiar with these (ex: \&, |, >>, etc.), consider reviewing \bluehref{https://www.geeksforgeeks.org/bitwise-operators-in-c-cpp/}{Geeks for Geek's explanation on the topic}. Bitwise operations are common topics in embedded software interviews, and it's essential to understand them. 25 | 26 | \subsubsection{Simple Approach} 27 | A simple approach to count the number of 1's in a byte is to iterate through each bit and check if it's set. Listing \ref{code:count-one-simple} shows a simple implementation of this approach. 28 | 29 | \lstinputlisting[caption={Simple Approach to Count Number of 1's in a Byte}, label={code:count-one-simple}]{code/bitstream_parity/count_ones_simple.c} 30 | 31 | \noindent While effective, this approach is not efficient. It requires 8 iterations to count the number of 1's in a byte. We can improve this by using a technique called \textit{Brian Kernighan's Algorithm}. 32 | 33 | \subsubsection{Brian Kernighan's Algorithm} 34 | Brian Kernighan's Algorithm is a technique to count the number of set bits in a byte. The algorithm works by repeatedly 'removing' the least significant $1$ bit and counting the number of iterations required to reach 0. Listing \ref{code:count-one-brian-kernighan} shows the implementation of this algorithm. It is more efficient than the simple approach as it only requires the number of set bits to be counted \cite{seander_bithacks}. 35 | 36 | \lstinputlisting[caption={Brian Kernighan's Algorithm to Count Number of 1's in a Byte}, label={code:count-one-brian-kernighan}]{code/bitstream_parity/count_ones_bk.c} 37 | 38 | \subsubsection{Faster Ways} 39 | There are faster ways to count the number of 1's in a byte, such as a lookup table to count the number of 1's for every byte. This approach is faster but requires more memory. Hamming Weight is another technique that counts the number of 1's in a byte in a more efficient manner, but it's more complex to implement (and usually out of scope for interviews). There's also usually a dedicated instruction, called \texttt{popcount}, that can count the number of set bits in one step, if supported by HW/compiler. More often than not, embedded interviews focus on the fundamentals and emphasize understanding over complex solutions. 40 | 41 | \subsection{Implementing Parity Bit Check} 42 | Putting it together, the steps to implement a parity bit are as follows: 43 | \begin{enumerate} 44 | \item For every byte, count the number of 1's (for this example, we'll use Brian Kernighan's Algorithm). 45 | \item Determine the expected parity bit based on the parity scheme (even or odd). 46 | \item Compare the parity bit to the expected parity bit. 47 | \end{enumerate} 48 | 49 | \noindent Listing \ref{code:bitstream-parity} shows the implementation of the \texttt{check\_parity} function that implements the steps mentioned above. 50 | 51 | \lstinputlisting[caption={Bitstream Parity Implementation}, label={code:bitstream-parity}]{code/bitstream_parity/bitstream_parity.c} 52 | 53 | \end{document} -------------------------------------------------------------------------------- /src/cabin.sty: -------------------------------------------------------------------------------- 1 | \NeedsTeXFormat{LaTeX2e} 2 | \ProvidesPackage{cabin} 3 | [2023/09/25 (Bob Tennent) Supports Cabin fonts for all LaTeX engines.] 4 | 5 | \RequirePackage{ifxetex,ifluatex,xkeyval,textcomp} 6 | 7 | \newif\ifcabin@otf 8 | \ifxetex 9 | \cabin@otftrue 10 | \else\ifluatex 11 | \cabin@otftrue 12 | \else % [pdf]LaTeX 13 | \cabin@otffalse 14 | \fi\fi 15 | 16 | \newif\ifcabin@default \cabin@defaultfalse 17 | \newif\ifcabin@lining \cabin@liningfalse 18 | \newif\ifcabin@tabular \cabin@tabularfalse 19 | \newif\ifcabin@semibold \cabin@semiboldfalse 20 | \newif\ifcabin@medium \cabin@mediumfalse 21 | \newif\ifcabin@condensed \cabin@condensedfalse 22 | 23 | \newcommand*{\Cabin@scale}{1} 24 | \DeclareOptionX{scaled}{\renewcommand*{\Cabin@scale}{#1}} 25 | \DeclareOptionX{scale}{\renewcommand*{\Cabin@scale}{#1}} 26 | 27 | \DeclareOptionX{default}{\cabin@defaulttrue} 28 | \DeclareOptionX{sfdefault}{\cabin@defaulttrue} 29 | \DeclareOptionX{type1}{\cabin@otffalse} 30 | \DeclareOptionX{semibold}{\cabin@semiboldtrue} 31 | \DeclareOptionX{bold}{\cabin@semiboldfalse} 32 | \DeclareOptionX{medium}{\cabin@mediumtrue} 33 | \DeclareOptionX{regular}{\cabin@mediumfalse} 34 | \DeclareOptionX{condensed}{\cabin@condensedtrue} 35 | 36 | \ExecuteOptionsX{bold,regular} 37 | \ProcessOptionsX\relax 38 | 39 | 40 | \ifcabin@otf 41 | \RequirePackage{fontspec} 42 | \else 43 | \RequirePackage{fontenc,fontaxes,mweights} 44 | \fi 45 | 46 | \ifcabin@otf 47 | 48 | \def\cabin@boldstyle{Bold} 49 | \ifcabin@semibold\def\cabin@boldstyle{SemiBold}\fi 50 | \def\cabin@regstyle{Regular} 51 | \ifcabin@medium\def\cabin@regstyle{Medium}\fi 52 | 53 | \else % type1 54 | 55 | \ifcabin@condensed 56 | \def\bfseries@sf{bc} 57 | \ifcabin@semibold\def\bfseries@sf{sbc}\fi 58 | \def\mdseries@sf{c} 59 | \ifcabin@medium\def\mdseries@sf{mediumcondensed}\fi 60 | \def\seriesdefault{\mdseries@sf} 61 | \else 62 | \def\bfseries@sf{b} 63 | \ifcabin@semibold\def\bfseries@sf{sb}\fi 64 | \def\mdseries@sf{m} 65 | \ifcabin@medium\def\mdseries@sf{medium}\fi 66 | \fi 67 | \fi 68 | 69 | \ifcabin@otf 70 | \ifxetex\XeTeXtracingfonts=1\fi 71 | \defaultfontfeatures{ 72 | Ligatures = TeX , 73 | Scale = \Cabin@scale , 74 | Extension = .otf } 75 | \def\cabin@regular{Regular} 76 | \def\cabin@semibold{SemiBold} 77 | \ifcabin@condensed 78 | \setsansfont 79 | [ UprightFont = *-\cabin@regstyle Condensed, 80 | ItalicFont = *-\ifx\cabin@regstyle\cabin@regular Italic\else\cabin@regstyle Italic\fi Condensed, 81 | BoldFont = *-\cabin@boldstyle Condensed, 82 | BoldItalicFont = *-\ifx\cabin@boldstyle\cabin@semibold Semibold\else\cabin@boldstyle\fi ItalicCondensed ] 83 | {CabinCondensed} 84 | \else 85 | \setsansfont 86 | [ UprightFont = *-\cabin@regstyle , 87 | ItalicFont = *-\ifx\cabin@regstyle\cabin@regular Italic\else\cabin@regstyle Italic\fi , 88 | BoldFont = *-\cabin@boldstyle , 89 | BoldItalicFont = *-\cabin@boldstyle Italic ] 90 | {Cabin} 91 | \fi 92 | % grab current family in case of subsequent change: 93 | \let\cabinfamily\sfdefault 94 | \ifcabin@default\renewcommand*\familydefault{\cabinfamily}\fi 95 | \newfontfamily\cabin 96 | [ UprightFont = *-\cabin@regstyle , 97 | ItalicFont = *-\ifx\cabin@regstyle\cabin@regular Italic\else\cabin@regstyle Italic\fi , 98 | BoldFont = *-\cabin@boldstyle , 99 | BoldItalicFont = *-\cabin@boldstyle Italic ] 100 | {Cabin} 101 | \newfontfamily\cabincondensed 102 | [ UprightFont = *-\cabin@regstyle Condensed, 103 | ItalicFont = *-\ifx\cabin@regstyle\cabin@regular Italic\else\cabin@regstyle Italic\fi Condensed, 104 | BoldFont = *-\cabin@boldstyle Condensed, 105 | BoldItalicFont = *-\ifx\cabin@boldstyle\cabin@semibold Semibold\else\cabin@boldstyle\fi ItalicCondensed ] 106 | {CabinCondensed} 107 | \else % type1 108 | \def\cabinfamily{Cabin-TLF} 109 | \newcommand*\cabin{\fontfamily{\cabinfamily}\selectfont} 110 | \newcommand*\cabincondensed{% 111 | \def\bfseries@sf{bc} 112 | \ifcabin@semibold\def\bfseries@sf{sbc}\fi 113 | \def\mdseries@sf{c} 114 | \ifcabin@medium\def\mdseries@sf{mediumcondensed}\fi 115 | \mdseries 116 | } 117 | \def\sfdefault{\cabinfamily} 118 | \ifcabin@default\edef\familydefault{\sfdefault}\edef\seriesdefault{\mdseries@sf}\fi 119 | \fi 120 | 121 | \ifcabin@otf 122 | % turn off defaults in case other fonts are selected: 123 | \defaultfontfeatures{} 124 | \fi 125 | 126 | \endinput 127 | -------------------------------------------------------------------------------- /src/code/adc_init/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.10) 2 | project(Generic_Library LANGUAGES C) 3 | 4 | # Automatically find all C source files in the current directory and subdirectories 5 | file(GLOB_RECURSE SOURCES "*.c") 6 | 7 | if(SOURCES) 8 | message(STATUS "Found source files: ${SOURCES}") 9 | else() 10 | message(WARNING "No source files found!") 11 | endif() 12 | 13 | 14 | # Create the static library 15 | add_library(project_lib STATIC ${SOURCES}) 16 | 17 | # Automatically set include directories based on directory structure 18 | target_include_directories(project_lib PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) 19 | 20 | # Enable all warnings and treat them as errors 21 | target_compile_options(project_lib PRIVATE 22 | -Wall # Enable all standard warnings 23 | -Wextra # Enable extra warnings 24 | -Wpedantic # Enforce strict compliance 25 | -Werror # Treat warnings as errors 26 | ) 27 | 28 | # Optional: Add a test executable to validate the build 29 | # It will look for main.c as a placeholder test 30 | if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/main.c") 31 | add_executable(test_app main.c) 32 | target_link_libraries(test_app PRIVATE project_lib) 33 | endif() 34 | -------------------------------------------------------------------------------- /src/code/adc_init/adc_conversion.c: -------------------------------------------------------------------------------- 1 | #include "adc_init_registers.h" 2 | #include 3 | #include 4 | 5 | bool adc_read_channel_V(uint8_t channel, float *voltage) { 6 | bool ret = false; 7 | const bool channel_valid = (channel <= MAX_ADC_CHANNEL); 8 | const bool adc_ready = 9 | (*ADC_STATUS_REG & (1 << BITPOS_ADC_CONVERSION_INITIALIZED)); 10 | if (channel_valid && adc_ready && (voltage != NULL)) { 11 | // reset the MUX_SEL bits - & with the inverse of the mask, which sets the 12 | // masked bits to 0 13 | *ADC_CONFIG_REG &= ~(MUX_SEL_MASK); 14 | // Set the MUX_SEL bits to the channel 15 | *ADC_CONFIG_REG |= (channel << BITPOS_ADC_MUX_SEL); 16 | // Start the conversion 17 | *ADC_CONFIG_REG |= (1 << BITPOS_ADC_CONVERSION_BEGIN); 18 | // Wait for the conversion to complete 19 | while ((*ADC_STATUS_REG & (1 << BITPOS_ADC_BUSY))) 20 | ; 21 | const uint16_t ADC_HIGH_VALUE = *ADC_DATA_HIGH_REG & ADC_DATA_HIGH_REG_MASK; 22 | // Read the ADC value. 23 | const uint16_t adc_value = (ADC_HIGH_VALUE << 8) | *ADC_DATA_LOW_REG; 24 | 25 | const uint16_t ADC_MAX = 4095; // 2^12 - 1 for 12-bit ADC 26 | *voltage = (adc_value * V_REF) / (float)ADC_MAX; 27 | ret = true; 28 | } 29 | return ret; 30 | } -------------------------------------------------------------------------------- /src/code/adc_init/adc_init.c: -------------------------------------------------------------------------------- 1 | #include "adc_init_registers.h" 2 | #include 3 | 4 | void adc_init(void) { 5 | // Set the prescaler to 3 6 | const uint8_t PRESCALER_1MHZ_ADC_CLK = 7; 7 | *ADC_CONFIG_REG = PRESCALER_1MHZ_ADC_CLK; 8 | 9 | // Enable the ADC 10 | *ADC_CONFIG_REG |= (1 << BITPOS_ADC_EN); 11 | 12 | // Wait for the ADC to be ready 13 | while ((*ADC_STATUS_REG & (1 << BITPOS_ADC_CONVERSION_INITIALIZED)) == false) 14 | ; 15 | } -------------------------------------------------------------------------------- /src/code/adc_init/adc_init_header.h: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | /** 4 | * @brief Initialize the ADC. 5 | * @note Blocking function 6 | */ 7 | void adc_init(void); 8 | /** 9 | * @brief Read the ADC value 10 | * @param channel The channel to read from 11 | * @param voltage The voltage at the specified channel 12 | * @return true if the read was successful, false otherwise 13 | */ 14 | bool adc_read_channel_V(uint8_t channel, float *voltage); -------------------------------------------------------------------------------- /src/code/adc_init/adc_init_registers.h: -------------------------------------------------------------------------------- 1 | #include 2 | const float V_REF = 3.3; 3 | 4 | volatile uint8_t *const ADC_CONFIG_REG = (uint8_t *)0x4000007C; 5 | const uint8_t BITPOS_ADC_EN = 7; 6 | const uint8_t BITPOS_ADC_CONVERSION_BEGIN = 6; 7 | const uint8_t BITPOS_ADC_MUX_SEL = 4; 8 | 9 | const uint8_t MAX_ADC_CHANNEL = 3; 10 | const uint8_t MUX_SEL_MASK = MAX_ADC_CHANNEL 11 | << BITPOS_ADC_MUX_SEL; // 0b00110000 12 | volatile uint8_t *const ADC_STATUS_REG = (uint8_t *)0x4000007D; 13 | const uint8_t BITPOS_ADC_CONVERSION_INITIALIZED = 7; 14 | const uint8_t BITPOS_ADC_BUSY = 0; 15 | 16 | volatile uint8_t *const ADC_DATA_HIGH_REG = (uint8_t *)0x4000007E; 17 | const uint8_t ADC_DATA_HIGH_REG_MASK = 0x0F; // 0b00001111 18 | volatile uint8_t *const ADC_DATA_LOW_REG = (uint8_t *)0x4000007F; -------------------------------------------------------------------------------- /src/code/bitbang_spi/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.10) 2 | project(Generic_Library LANGUAGES C) 3 | 4 | # Automatically find all C source files in the current directory and subdirectories 5 | file(GLOB_RECURSE SOURCES "*.c") 6 | 7 | if(SOURCES) 8 | message(STATUS "Found source files: ${SOURCES}") 9 | else() 10 | message(WARNING "No source files found!") 11 | endif() 12 | 13 | 14 | # Create the static library 15 | add_library(project_lib STATIC ${SOURCES}) 16 | 17 | # Automatically set include directories based on directory structure 18 | target_include_directories(project_lib PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) 19 | 20 | # Enable all warnings and treat them as errors 21 | target_compile_options(project_lib PRIVATE 22 | -Wall # Enable all standard warnings 23 | -Wextra # Enable extra warnings 24 | -Wpedantic # Enforce strict compliance 25 | -Werror # Treat warnings as errors 26 | ) 27 | 28 | # Optional: Add a test executable to validate the build 29 | # It will look for main.c as a placeholder test 30 | if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/main.c") 31 | add_executable(test_app main.c) 32 | target_link_libraries(test_app PRIVATE project_lib) 33 | endif() 34 | -------------------------------------------------------------------------------- /src/code/bitbang_spi/bitbang_ex.c: -------------------------------------------------------------------------------- 1 | #include "bitbang_ex.h" 2 | 3 | void square_wave(uint8_t pin, uint8_t cycles, uint8_t period_us) { 4 | for (uint8_t i = 0; i < cycles; i++) { 5 | HAL_GPIO_WritePin(pin, HIGH); 6 | delayMicroseconds(period_us / 2); 7 | HAL_GPIO_WritePin(pin, LOW); 8 | delayMicroseconds(period_us / 2); 9 | } 10 | } -------------------------------------------------------------------------------- /src/code/bitbang_spi/bitbang_ex.h: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #define HIGH 1 4 | #define LOW 0 5 | 6 | void HAL_GPIO_WritePin(uint8_t pin, uint8_t state); 7 | void delayMicroseconds(uint8_t us); -------------------------------------------------------------------------------- /src/code/bitbang_spi/bitbang_spi.c: -------------------------------------------------------------------------------- 1 | #include "bitbang_spi_header.h" 2 | 3 | #define NUM_BITS_IN_BYTE 8 4 | #define NS_PER_SECOND 1000000000 5 | 6 | bool bitbang_spi_transceive(float clk_freq_hz, uint8_t const *const tx_data, 7 | uint8_t *const rx_data, size_t len_bytes) { 8 | const bool ret = (tx_data != NULL) && (rx_data != NULL) && (len_bytes > 0); 9 | 10 | if (ret) { 11 | // clock calcs - integer division is used, but error is acceptable 12 | const uint32_t period_ns = NS_PER_SECOND / clk_freq_hz; 13 | const uint32_t half_period_ns = period_ns / 2; 14 | 15 | HAL_GPIO_write(PIN_NUM_SCLK, true); 16 | HAL_GPIO_write(PIN_NUM_CS, false); 17 | delay_nanoseconds(period_ns); // CS setup time, arbitrary value 18 | 19 | for (uint32_t byte = 0; byte < len_bytes; byte++) { 20 | const uint8_t tx_byte = tx_data[byte]; 21 | for (uint32_t bit = 0; bit < NUM_BITS_IN_BYTE; bit++) { 22 | // falling edge - write MOSI output here 23 | HAL_GPIO_write(PIN_NUM_SCLK, false); 24 | const uint8_t tx_bit = (tx_byte >> (7 - bit)) & 0x01; 25 | HAL_GPIO_write(PIN_NUM_MOSI, (bool)tx_bit); 26 | delay_nanoseconds(half_period_ns); 27 | // rising edge - read MISO input here 28 | HAL_GPIO_write(PIN_NUM_SCLK, true); 29 | delay_nanoseconds(half_period_ns); 30 | rx_data[byte] |= HAL_GPIO_read(PIN_NUM_MISO) << (7 - bit); 31 | } 32 | } 33 | 34 | HAL_GPIO_write(PIN_NUM_CS, true); 35 | } 36 | 37 | return ret; 38 | } -------------------------------------------------------------------------------- /src/code/bitbang_spi/bitbang_spi_header.h: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | typedef enum { PIN_NUM_CS, PIN_NUM_SCLK, PIN_NUM_MISO, PIN_NUM_MOSI } spi_pin_E; 6 | 7 | /** Externally Provided Functions - these are assumed to be error free **/ 8 | void HAL_GPIO_write(spi_pin_E pin, bool value); 9 | bool HAL_GPIO_read(spi_pin_E pin); // Returns the logical value of the pin 10 | void delay_nanoseconds(uint32_t ns); 11 | 12 | /** USER IMPLEMENTED FUNCTIONS **/ 13 | /** 14 | * @brief Transmits and receives data over SPI using bit-banging 15 | * @param clk_freq_hz The frequency of the SPI clock in Hz 16 | * @param tx_data Pointer to the data to be transmitted 17 | * @param rx_data Pointer to the buffer to store the received data 18 | * @param len_bytes The number of bytes to transmit and receive. The length of 19 | * tx_data and rx_data must be at least len 20 | * @return true if the transaction was successful, false otherwise 21 | */ 22 | bool bitbang_spi_transceive(float clk_freq_hz, uint8_t const *const tx_data, 23 | uint8_t *const rx_data, size_t len_bytes); 24 | -------------------------------------------------------------------------------- /src/code/bitstream_parity/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.10) 2 | project(Generic_Library LANGUAGES C) 3 | 4 | # Automatically find all C source files in the current directory and subdirectories 5 | file(GLOB_RECURSE SOURCES "*.c") 6 | 7 | if(SOURCES) 8 | message(STATUS "Found source files: ${SOURCES}") 9 | else() 10 | message(WARNING "No source files found!") 11 | endif() 12 | 13 | 14 | # Create the static library 15 | add_library(project_lib STATIC ${SOURCES}) 16 | 17 | # Automatically set include directories based on directory structure 18 | target_include_directories(project_lib PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) 19 | 20 | # Enable all warnings and treat them as errors 21 | target_compile_options(project_lib PRIVATE 22 | -Wall # Enable all standard warnings 23 | -Wextra # Enable extra warnings 24 | -Wpedantic # Enforce strict compliance 25 | -Werror # Treat warnings as errors 26 | ) 27 | 28 | # Optional: Add a test executable to validate the build 29 | # It will look for main.c as a placeholder test 30 | if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/main.c") 31 | add_executable(test_app main.c) 32 | target_link_libraries(test_app PRIVATE project_lib) 33 | endif() 34 | -------------------------------------------------------------------------------- /src/code/bitstream_parity/bitstream_parity.c: -------------------------------------------------------------------------------- 1 | #include "bitstream_parity.h" 2 | #include "count_ones_bk.h" 3 | 4 | bool bitstream_parity_valid(uint8_t *bitstream, uint32_t length, 5 | bool parity_bit, bitstream_parity_E scheme) { 6 | 7 | uint32_t ones = 0U; 8 | // Count the number of ones in the bitstream 9 | for (uint32_t i = 0U; i < length; i++) { 10 | ones += count_ones_bk(bitstream[i]); 11 | } 12 | bool expected_parity = false; 13 | const bool number_of_ones_is_odd = (ones & 1U); // & 1U is equivalent to % 2 14 | switch (scheme) { 15 | case BITSTREAM_PARITY_EVEN: 16 | expected_parity = (number_of_ones_is_odd) == true; 17 | break; 18 | case BITSTREAM_PARITY_ODD: 19 | expected_parity = (number_of_ones_is_odd) == false; 20 | break; 21 | default: 22 | return false; 23 | break; 24 | } 25 | 26 | return expected_parity == parity_bit; 27 | } -------------------------------------------------------------------------------- /src/code/bitstream_parity/bitstream_parity.h: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | typedef enum { 6 | BITSTREAM_PARITY_EVEN, // Parity bit = 0 if number of ones in bitstream is 7 | // even, 1 otherwise 8 | BITSTREAM_PARITY_ODD, // Parity bit = 0 if number of ones in bitstream is 9 | // odd, 1 otherwise 10 | } bitstream_parity_E; 11 | 12 | /** 13 | * @brief Check if the parity of the bitstream is valid 14 | * @param bitstream The byte array to check 15 | * @param length The length of the byte array 16 | * @param parity_bit The parity bit to check against 17 | * @param scheme The parity to use for the check 18 | * @return true if the bitstream has the correct parity, false otherwise 19 | */ 20 | bool bitstream_parity_valid(uint8_t *bitstream, uint32_t length, 21 | bool parity_bit, bitstream_parity_E scheme); -------------------------------------------------------------------------------- /src/code/bitstream_parity/count_ones_bk.c: -------------------------------------------------------------------------------- 1 | #include "count_ones_bk.h" 2 | 3 | uint8_t count_ones_bk(uint8_t byte) { 4 | uint8_t count = 0; 5 | while (byte != 0) { 6 | byte &= (byte - 1); 7 | count++; 8 | } 9 | return count; 10 | } -------------------------------------------------------------------------------- /src/code/bitstream_parity/count_ones_bk.h: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | uint8_t count_ones_bk(uint8_t byte); -------------------------------------------------------------------------------- /src/code/bitstream_parity/count_ones_simple.c: -------------------------------------------------------------------------------- 1 | #include "bitstream_parity.h" 2 | 3 | uint8_t count_ones_simple(uint8_t byte) { 4 | uint8_t count = 0; 5 | for (size_t i = 0; i < 8; i++) { 6 | const uint8_t mask = 1 << i; 7 | if (byte & mask) { 8 | count++; 9 | } 10 | } 11 | return count; 12 | } -------------------------------------------------------------------------------- /src/code/keyword/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.10) 2 | project(Generic_Library LANGUAGES C) 3 | 4 | # Automatically find all C source files in the current directory and subdirectories 5 | file(GLOB_RECURSE SOURCES "*.c") 6 | 7 | if(SOURCES) 8 | message(STATUS "Found source files: ${SOURCES}") 9 | else() 10 | message(WARNING "No source files found!") 11 | endif() 12 | 13 | 14 | # Create the static library 15 | add_library(project_lib STATIC ${SOURCES}) 16 | 17 | # Automatically set include directories based on directory structure 18 | target_include_directories(project_lib PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) 19 | 20 | # Enable all warnings and treat them as errors 21 | target_compile_options(project_lib PRIVATE 22 | -Wall # Enable all standard warnings 23 | -Wextra # Enable extra warnings 24 | -Wpedantic # Enforce strict compliance 25 | -Werror # Treat warnings as errors 26 | ) 27 | 28 | # Optional: Add a test executable to validate the build 29 | # It will look for main.c as a placeholder test 30 | if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/main.c") 31 | add_executable(test_app main.c) 32 | target_link_libraries(test_app PRIVATE project_lib) 33 | endif() 34 | -------------------------------------------------------------------------------- /src/code/keyword/main.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | void some_function(void) { 5 | static uint32_t counter = 0; // Static local variable 6 | printf("%u ", counter); 7 | counter++; 8 | } 9 | 10 | int main(void) { 11 | for (uint32_t i = 0; i < 5; i++) { 12 | some_function(); 13 | } 14 | // prints: 0 1 2 3 4 15 | return 0; 16 | } -------------------------------------------------------------------------------- /src/code/keyword/volatile_ex.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | uint32_t shared_counter = 0; // Shared variable 5 | 6 | // Interrupt Service Routine (ISR) updates the counter 7 | void ISR_Timer(void) { 8 | // called every 1ms 9 | shared_counter++; // Increment counter 10 | } 11 | 12 | // Main loop checks the counter 13 | int main(void) { 14 | while (1) { 15 | if (shared_counter % 1000 == 0) { 16 | // Do something every 1 second 17 | } 18 | } 19 | return 0; 20 | } -------------------------------------------------------------------------------- /src/code/packet_parsing/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.10) 2 | project(Generic_Library LANGUAGES C) 3 | 4 | # Automatically find all C source files in the current directory and subdirectories 5 | file(GLOB_RECURSE SOURCES "*.c") 6 | 7 | if(SOURCES) 8 | message(STATUS "Found source files: ${SOURCES}") 9 | else() 10 | message(WARNING "No source files found!") 11 | endif() 12 | 13 | 14 | # Create the static library 15 | add_library(project_lib STATIC ${SOURCES}) 16 | 17 | # Automatically set include directories based on directory structure 18 | target_include_directories(project_lib PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) 19 | 20 | # Enable all warnings and treat them as errors 21 | target_compile_options(project_lib PRIVATE 22 | -Wall # Enable all standard warnings 23 | -Wextra # Enable extra warnings 24 | -Wpedantic # Enforce strict compliance 25 | -Werror # Treat warnings as errors 26 | -Og # Enable debug optimizations 27 | ) 28 | 29 | # Optional: Add a test executable to validate the build 30 | # It will look for main.c as a placeholder test 31 | if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/main.c") 32 | add_executable(test_app main.c) 33 | target_link_libraries(test_app PRIVATE project_lib) 34 | endif() 35 | -------------------------------------------------------------------------------- /src/code/packet_parsing/checksum_ex.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | bool verify_checksum(const uint8_t *buf, size_t len, uint16_t expected) { 6 | uint16_t checksum = 0; // note: overflow possibility if len > 255 7 | for (size_t i = 0; i < len; i++) { 8 | checksum += buf[i]; 9 | } 10 | 11 | return checksum == expected; 12 | } -------------------------------------------------------------------------------- /src/code/packet_parsing/main.c: -------------------------------------------------------------------------------- 1 | #include "packet_parsing_header.h" 2 | #include 3 | #include 4 | 5 | // Use a macro to compare floating point numbers since == is not reliable for 6 | // floats 7 | #define FLOAT_EQUALS(a, b) (fabsf(a - b) < 0.0001f) 8 | 9 | bool test_invalid_args(void) { 10 | weather_data_t weather_data; 11 | uint8_t data[8U] = {0}; 12 | bool success = parse_packet(NULL, 0, &weather_data); 13 | success |= parse_packet(data, 0, &weather_data); 14 | success |= parse_packet(data, 8, NULL); 15 | 16 | return !success; 17 | } 18 | 19 | bool test_incorrect_SOF(void) { 20 | uint8_t packet[] = {0x54, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x54}; 21 | 22 | weather_data_t weather_data; 23 | bool success = parse_packet(packet, sizeof(packet), &weather_data); 24 | 25 | return !success; 26 | } 27 | 28 | bool test_incorrect_checksum(void) { 29 | uint8_t packet[] = {0x55, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x53}; 30 | 31 | weather_data_t weather_data; 32 | bool success = parse_packet(packet, sizeof(packet), &weather_data); 33 | 34 | return !success; 35 | } 36 | 37 | bool test_correct_packet(void) { 38 | uint8_t packet[] = {0x55, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; 39 | 40 | uint8_t fake_temperature_degC = 12U; 41 | packet[1] = fake_temperature_degC + 32U; 42 | 43 | uint8_t fake_temperature_fraction_degC = 5U; 44 | packet[2] = fake_temperature_fraction_degC; 45 | 46 | uint32_t fake_pressure_pA = 101325U; 47 | packet[3] = (fake_pressure_pA >> 24) & 0xFF; 48 | packet[4] = (fake_pressure_pA >> 16) & 0xFF; 49 | packet[5] = (fake_pressure_pA >> 8) & 0xFF; 50 | packet[6] = fake_pressure_pA & 0xFF; 51 | 52 | uint32_t checksum = 0U; 53 | for (size_t i = 0; i < 7; i++) { 54 | checksum += packet[i]; 55 | } 56 | 57 | packet[7] = checksum & 0xFF; 58 | 59 | weather_data_t weather_data; 60 | bool success = parse_packet(packet, sizeof(packet), &weather_data); 61 | 62 | success &= (FLOAT_EQUALS(weather_data.temperature_degC, 12.5f)); 63 | success &= (FLOAT_EQUALS(weather_data.pressure_kPa, 64 | (float)fake_pressure_pA / 1000.0f)); 65 | 66 | printf("Temperature: %.2f degC\n", weather_data.temperature_degC); 67 | printf("Pressure: %.2f kPa\n", weather_data.pressure_kPa); 68 | 69 | return success; 70 | } 71 | 72 | int main() { 73 | bool success = true; 74 | if (test_invalid_args() == false) { 75 | printf("test_invalid_args failed\n"); 76 | success = false; 77 | } 78 | 79 | if (test_incorrect_SOF() == false) { 80 | printf("test_incorrect_SOF failed\n"); 81 | success = false; 82 | } 83 | 84 | if (test_incorrect_checksum() == false) { 85 | printf("test_incorrect_checksum failed\n"); 86 | success = false; 87 | } 88 | 89 | if (test_correct_packet() == false) { 90 | printf("test_correct_packet failed\n"); 91 | success = false; 92 | } 93 | 94 | if (success) { 95 | printf("All tests passed\n"); 96 | } 97 | 98 | return success ? 0 : 1; 99 | } -------------------------------------------------------------------------------- /src/code/packet_parsing/packet_parsing_header.h: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | typedef struct { 6 | float temperature_degC; 7 | float pressure_kPa; 8 | } weather_data_t; 9 | 10 | /** 11 | * @brief Parse a packet of data and extract the weather data 12 | * @param packet The packet of data to parse 13 | * @param len The length of the received packet 14 | * @param weather_data The weather data to populate 15 | * @return true if the packet was parsed successfully, false otherwise 16 | */ 17 | bool parse_packet(const uint8_t *packet, size_t len, 18 | weather_data_t *weather_data); -------------------------------------------------------------------------------- /src/code/packet_parsing/parsing_implementation.c: -------------------------------------------------------------------------------- 1 | #include "packet_parsing_header.h" 2 | #define NUM_BITS_IN_BYTE (8U) 3 | 4 | bool parse_packet(const uint8_t *packet, size_t len, 5 | weather_data_t *weather_data) { 6 | bool success = false; 7 | // Let the compiler do the work of calculating len 8 | const size_t expected_len = (1U + 2U + 4U + 1U); 9 | if ((len == expected_len) && (packet != NULL) && (weather_data != NULL)) { 10 | const bool SOF_match = (packet[0] == 0x55); 11 | 12 | uint16_t sum_of_bytes = 0; 13 | for (size_t i = 0; (i < 7) && SOF_match; i++) { 14 | sum_of_bytes += packet[i]; 15 | } 16 | const uint8_t received_checksum = packet[7]; 17 | // Only compare the least significant byte of the sum 18 | const bool checksum_match = ((sum_of_bytes & 0xFF) == received_checksum); 19 | success = checksum_match && SOF_match; 20 | 21 | if (success) { 22 | float temperature_degC = 0.0f; 23 | temperature_degC += (float)packet[1] - 32.0f; // integer part; 24 | temperature_degC += (float)packet[2] / 10.0f; // fractional part; 25 | float pressure_Pa = 0.0f; 26 | pressure_Pa += (float)((uint32_t)packet[3] << (NUM_BITS_IN_BYTE * 3)); 27 | pressure_Pa += (float)((uint32_t)packet[4] << (NUM_BITS_IN_BYTE * 2)); 28 | pressure_Pa += (float)((uint32_t)packet[5] << (NUM_BITS_IN_BYTE * 1)); 29 | pressure_Pa += (float)(packet[6]); 30 | weather_data->temperature_degC = temperature_degC; 31 | weather_data->pressure_kPa = pressure_Pa / 1000.0f; // Convert Pa to kPa 32 | } 33 | } 34 | return success; 35 | } -------------------------------------------------------------------------------- /src/code/pid/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.10) 2 | project(Generic_Library LANGUAGES C) 3 | 4 | # Automatically find all C source files in the current directory and subdirectories 5 | file(GLOB_RECURSE SOURCES "*.c") 6 | 7 | if(SOURCES) 8 | message(STATUS "Found source files: ${SOURCES}") 9 | else() 10 | message(WARNING "No source files found!") 11 | endif() 12 | 13 | 14 | # Create the static library 15 | add_library(project_lib STATIC ${SOURCES}) 16 | 17 | # Automatically set include directories based on directory structure 18 | target_include_directories(project_lib PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) 19 | 20 | # Enable all warnings and treat them as errors 21 | target_compile_options(project_lib PRIVATE 22 | -Wall # Enable all standard warnings 23 | -Wextra # Enable extra warnings 24 | -Wpedantic # Enforce strict compliance 25 | -Werror # Treat warnings as errors 26 | ) 27 | 28 | # Optional: Add a test executable to validate the build 29 | # It will look for main.c as a placeholder test 30 | if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/main.c") 31 | add_executable(test_app main.c) 32 | target_link_libraries(test_app PRIVATE project_lib) 33 | endif() 34 | -------------------------------------------------------------------------------- /src/code/pid/pid_controller.c: -------------------------------------------------------------------------------- 1 | #include "pid_typedef.h" 2 | #include 3 | 4 | void pid_init(pid_t *pid) { 5 | if (pid != NULL) { 6 | pid->integral = 0.0F; 7 | pid->prev_error = 0.0F; 8 | } 9 | } 10 | 11 | float pid_step(pid_t *pid, float setpoint, float measured_output, float dt) { 12 | float ret = 0.0F; 13 | // Check for NULL pointer and positive time step 14 | if ((pid != NULL) && (dt > 0.0F)) { 15 | const float error = setpoint - measured_output; 16 | pid->integral += error * dt; 17 | 18 | const float p_term = pid->kp * error; 19 | const float i_term = pid->ki * pid->integral; 20 | const float d_term = pid->kd * (error - pid->prev_error) / dt; 21 | 22 | pid->prev_error = error; 23 | ret = p_term + i_term + d_term; 24 | } 25 | return ret; 26 | } -------------------------------------------------------------------------------- /src/code/pid/pid_typedef.h: -------------------------------------------------------------------------------- 1 | typedef struct { 2 | float kp; // Proportional gain constant 3 | float ki; // Integral gain constant 4 | float kd; // Derivative gain constant 5 | 6 | float integral; // Stored integral value 7 | float prev_error; // Stored last input value 8 | } pid_t; -------------------------------------------------------------------------------- /src/code/stack/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.10) 2 | project(Generic_Library LANGUAGES C) 3 | 4 | # Automatically find all C source files in the current directory and subdirectories 5 | file(GLOB_RECURSE SOURCES "*.c") 6 | 7 | if(SOURCES) 8 | message(STATUS "Found source files: ${SOURCES}") 9 | else() 10 | message(WARNING "No source files found!") 11 | endif() 12 | 13 | 14 | # Create the static library 15 | add_library(project_lib STATIC ${SOURCES}) 16 | 17 | # Automatically set include directories based on directory structure 18 | target_include_directories(project_lib PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) 19 | 20 | # Enable all warnings and treat them as errors 21 | target_compile_options(project_lib PRIVATE 22 | -Wall # Enable all standard warnings 23 | -Wextra # Enable extra warnings 24 | -Wpedantic # Enforce strict compliance 25 | -Og # Enable debug optimizations 26 | ) 27 | 28 | # Optional: Add a test executable to validate the build 29 | # It will look for main.c as a placeholder test 30 | if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/main.c") 31 | add_executable(test_app main.c) 32 | target_link_libraries(test_app PRIVATE project_lib) 33 | endif() 34 | -------------------------------------------------------------------------------- /src/code/stack/main.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | void check_stack_dir(uint8_t *comparison) { 5 | uint8_t local = 0; 6 | if (&local < comparison) { 7 | // Stack grows down 8 | printf("Stack grows down\n"); 9 | } else { 10 | // Stack grows up 11 | printf("Stack grows up\n"); 12 | } 13 | } 14 | 15 | int main() { 16 | uint8_t comparison = 0; 17 | check_stack_dir(&comparison); 18 | return 0; 19 | } -------------------------------------------------------------------------------- /src/code/stack/stack_ex/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.10) 2 | project(Generic_Library LANGUAGES C) 3 | 4 | # Automatically find all C source files in the current directory and subdirectories 5 | file(GLOB_RECURSE SOURCES "*.c") 6 | 7 | if(SOURCES) 8 | message(STATUS "Found source files: ${SOURCES}") 9 | else() 10 | message(WARNING "No source files found!") 11 | endif() 12 | 13 | 14 | # Create the static library 15 | add_library(project_lib STATIC ${SOURCES}) 16 | 17 | # Automatically set include directories based on directory structure 18 | target_include_directories(project_lib PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) 19 | 20 | # Enable all warnings and treat them as errors 21 | target_compile_options(project_lib PRIVATE 22 | -Wall # Enable all standard warnings 23 | -Wextra # Enable extra warnings 24 | -Wpedantic # Enforce strict compliance 25 | -Werror # Treat warnings as errors 26 | -Og # Enable debug optimizations 27 | ) 28 | 29 | # Optional: Add a test executable to validate the build 30 | # It will look for main.c as a placeholder test 31 | if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/main.c") 32 | add_executable(test_app main.c) 33 | target_link_libraries(test_app PRIVATE project_lib) 34 | endif() 35 | -------------------------------------------------------------------------------- /src/code/stack/stack_ex/main.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | uint8_t multiply(uint8_t c, uint8_t d) { return c * d; } 5 | 6 | uint8_t add_and_multiply(uint8_t a, uint8_t b) { 7 | return a + b + multiply(a, b); 8 | } 9 | 10 | int main() { 11 | uint8_t a = 10; 12 | uint8_t b = 20; 13 | uint8_t c = add_and_multiply(a, b); 14 | printf("Result: %u\n", c); 15 | return 0; 16 | } 17 | -------------------------------------------------------------------------------- /src/code/timer/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.10) 2 | project(Generic_Library LANGUAGES C) 3 | 4 | # Automatically find all C source files in the current directory and subdirectories 5 | file(GLOB_RECURSE SOURCES "*.c") 6 | 7 | if(SOURCES) 8 | message(STATUS "Found source files: ${SOURCES}") 9 | else() 10 | message(WARNING "No source files found!") 11 | endif() 12 | 13 | 14 | # Create the static library 15 | add_library(project_lib STATIC ${SOURCES}) 16 | 17 | # Automatically set include directories based on directory structure 18 | target_include_directories(project_lib PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) 19 | 20 | # Enable all warnings and treat them as errors 21 | target_compile_options(project_lib PRIVATE 22 | -Wall # Enable all standard warnings 23 | -Wextra # Enable extra warnings 24 | -Wpedantic # Enforce strict compliance 25 | -Werror # Treat warnings as errors 26 | ) 27 | 28 | # Optional: Add a test executable to validate the build 29 | # It will look for main.c as a placeholder test 30 | if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/main.c") 31 | add_executable(test_app main.c) 32 | target_link_libraries(test_app PRIVATE project_lib) 33 | endif() 34 | -------------------------------------------------------------------------------- /src/code/timer/timer_64bit_correct.c: -------------------------------------------------------------------------------- 1 | #include "timer_registers.h" 2 | 3 | uint64_t get_64_bit_time(void) { 4 | uint64_t high = *CNT_HIGH; 5 | uint64_t low = *CNT_LOW; 6 | // check if the high word has changed since reading low 7 | if (high != *CNT_HIGH) { 8 | // roll-over occurred between reading low and high, read low again 9 | low = *CNT_LOW; 10 | high = *CNT_HIGH; 11 | } 12 | const uint64_t time = ((uint64_t)high << 32) | low; 13 | return time; 14 | } -------------------------------------------------------------------------------- /src/code/timer/timer_64bit_naive.c: -------------------------------------------------------------------------------- 1 | #include "timer_registers.h" 2 | 3 | uint64_t get_64_bit_time(void) { 4 | uint64_t time = 0; 5 | time |= *CNT_LOW; 6 | time |= ((uint64_t)*CNT_HIGH) << 32; 7 | return time; 8 | } -------------------------------------------------------------------------------- /src/code/timer/timer_registers.h: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | volatile uint32_t *CNT_LOW = (uint32_t *)0x40000000; 4 | volatile uint32_t *CNT_HIGH = (uint32_t *)0x40000004; 5 | 6 | /** 7 | * @brief Get the 64 bit time from the timer 8 | * @return uint64_t The 64 bit time 9 | */ 10 | uint64_t get_64_bit_time(void); -------------------------------------------------------------------------------- /src/compare-i2c-spi.tex: -------------------------------------------------------------------------------- 1 | \documentclass[main.tex]{subfiles} 2 | \begin{document} 3 | 4 | \section{Compare and contrast I2C \textit{(Inter-Integrated Circuit)} and SPI \textit{(Serial Peripheral Interface)}.} \label{section:compare_i2c_spi} 5 | % Note this question is meant to give more details on these two protocols that are initially introduced as a part of signalling.tex which is intended to be a much broader question. 6 | 7 | \spoilerline 8 | 9 | \subsection{I2C} 10 | I2C is a \textit{half-duplex} (can either transmit or receive, but not both simultaneously) digital protocol developed by Phillips in 1982 \cite{sparkfun_i2c_history}. It enables a host device\footnote{I2C does support multiple master devices, however, this article focuses on the significantly more prevalent single-master implementation.} (referred to as a \textit{master}\footnote{The phrases 'master' and 'slave' are slowly being phased out due to their origins. However, at time of writing, 'master' and 'slave' are the most commonly used terms and are used in this guide. Alternative verbiage includes 'controller' for master and 'peripheral' or 'target' for slave.}) to communicate with multiple peripheral devices (referred to as \textit{slaves}) over a two-wire serial bus. 11 | % Unfortunately at the time of writing, master/slave is the pervasive industry standard terms. 12 | 13 | \subsubsection{Physical Layer} 14 | The physical layer of I2C consists of two wires: SDA (Serial Data) and SCL (Serial Clock). By definition, it is a \textit{synchronous} protocol, meaning that the clock signal is shared between the master and slave devices. The SDA line carries the data, while the SCL line carries the clock signal. The SDA line is bidirectional, allowing both the master and slave to transmit and receive data. The SCL line is unidirectional, controlled by the master device (though it can be asserted by a slave to pause communications to give time for processing, known as \textit{clock stretching}). A hardware bus diagram is shown in Figure \ref{fig:i2c_bus}. 15 | 16 | \begin{figure}[H] 17 | \centering 18 | \includegraphics[scale=0.09]{images/I2C_controller-target.png} 19 | \caption{I2C Bus Diagram, Source: Wikipedia \cite{wikipedia_i2c_bus_image}} 20 | \label{fig:i2c_bus} 21 | \end{figure} 22 | 23 | \noindent The bidirectional nature of the SDA and SCL lines is achieved by using \textit{open-drain} drivers. Open-drain drivers can pull the line low, but not drive it high, instead relying on a \textit{pull-up} resistor to "pull the voltage up". This is contrast to a \textit{push-pull} driver, which can drive the line both high and low. Open-drain drivers, while advantageous in ensuring that the bus can never be shorted by two devices driving the line with different voltages, suffers from slower rise/fall times due to the pull-up resistor forming an RC circuit with the parasitic bus capacitance, and limits the maximum bus speed to 400kHz traditionally, though higher speeds just above 1 MHz are permissible in newer versions of the specification. 24 | % More information about RC circuit diagram is not given here as it will be covered in "passive_circuits.tex" 25 | 26 | \subsubsection{Data Format} 27 | The data format of I2C features the following components, and is shown graphically in Figure \ref{fig:i2c_data_format}: 28 | \begin{enumerate} 29 | \item \textbf{Start Condition}: The master device initiates communication by pulling the SDA line low while the SCL line is high, signalling the beginning of a transfer. 30 | \item \textbf{Address (7 bits)}: A 7-bit address is transmitted on the SDA line to indicate which slave device the master wishes to communicate with. 31 | \item \textbf{Read/Write Bit}: The 8th bit of the address byte is used to indicate whether the master wishes to read from or write to the slave device. A \texttt{0} indicates a write operation, while a \texttt{1} indicates a read operation. If a read is requested, control of the SDA line is transferred to the slave device. 32 | \item \textbf{Acknowledge Bit}: After each byte is transmitted, the receiving device (master or slave) sends an acknowledge bit. If the receiving device pulls the SDA line low, it indicates that it has received the byte and is ready for the next byte. If the SDA line remains high, it indicates that the receiving device is not ready, or that an error occurred. 33 | \item \textbf{Data Byte(s)}: Data bytes are transmitted in 8-bit chunks, with each byte followed by an acknowledge bit. 34 | \item \textbf{Stop Condition}: The master device signals the end of the transfer by, in the case of a write, releasing the SDA line while the SCL line is high, or in the case of a read, sending a \texttt{NACK} (Not Acknowledge) bit followed by a stop condition. 35 | \end{enumerate} 36 | 37 | \begin{figure}[H] 38 | \centering 39 | \includegraphics[scale=0.4]{generated_images/svg_generated/i2c_data_format.png} 40 | \caption{2 Byte I2C Data Frame Format} 41 | \label{fig:i2c_data_format} 42 | \end{figure} 43 | 44 | \subsection{SPI} 45 | SPI was developed by Motorola \cite{SPI_history} and is a \textit{full-duplex} (simultaneous transmit and receive) synchronous serial communication protocol. It is commonly used in embedded systems to communicate between a master device and one or more slave devices. SPI is a \textit{four-wire} protocol, consisting of the following signals: MISO (Master In Slave Out), MOSI (Master Out Slave In), SCLK, and CS (Chip Select). A timing diagram of a 1-byte SPI transaction is shown in Figure \ref{fig:spi_timing}. 46 | 47 | \begin{figure}[H] 48 | \centering 49 | \includegraphics[scale=0.4]{generated_images/svg_generated/spi_waveform.png} 50 | \caption{1-byte SPI Timing Diagram} 51 | \label{fig:spi_timing} 52 | \end{figure} 53 | 54 | \noindent Unlike I2C, SPI does not have a standard addressing scheme, and the master device must \textit{assert} (pull down) a CS line to connected to the slave device it wishes to communicate with - the requirement for each slave device to feature its own CS line increases SPI's wiring complexity. Owing to the push-pull nature of SPI drivers, the bus is faster than I2C (low MHz range). The bus diagram is shown in Figure \ref{fig:spi_bus} (Note the diagram in Figure \ref{fig:spi_bus} uses \textit{SS} for \textit{Slave Select}, which is synonymous with \textit{CS}). 55 | 56 | \begin{figure}[H] 57 | \centering 58 | \includegraphics[scale=0.2]{generated_images/svg_generated/SPI_three_slaves.png} 59 | \caption{SPI Bus Diagram, Source: Wikipedia \cite{wikipedia_SPI_bus}} 60 | \label{fig:spi_bus} 61 | \end{figure} 62 | 63 | \subsection{Comparison} 64 | \begin{itemize} 65 | \item \textbf{Bus Speed}: SPI is faster than I2C, with speeds in the MHz range (ex: 1-40 MHz) compared to I2C's 400 kHz range due to the differences in drive type. As a result, SPI is often used in applications where the transfer speed is required to be high. 66 | \item \textbf{Wiring Complexity}: I2C requires, at most, two wires, regardless of the number of devices on the bus owing to its addressing scheme. A SPI master requires at least four wires, plus an additional wire for each slave device (each slave will require 4 wires). 67 | \item \textbf{Frame Format}: I2C has a more complex frame format than SPI, with start and stop conditions, address bytes, and acknowledge bits, which can assist in debugging unresponsive slave devices. However, SPI has a simpler frame format, with no addressing scheme and no acknowledge bits, which reduces the overhead of each transaction and affords more flexibility in the data format. 68 | % Comparison of I2C and SPI with analog signals is further elaborated on in signalling.tex 69 | \end{itemize} 70 | 71 | \subsection{Follow-ups} 72 | \begin{itemize} 73 | \item What are strategies for dealing with conflicting I2C addresses? 74 | \item What are strategies for dealing with the possibly-large number of CS lines in SPI? 75 | \item How are pull-up resistance values selected for I2C? 76 | \end{itemize} 77 | 78 | \end{document} 79 | -------------------------------------------------------------------------------- /src/diagram_generator/buck_ccm.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import matplotlib.pyplot as plt 3 | import argparse 4 | 5 | # fmt: off 6 | 7 | TITLE = "Buck Converter in Continuous Conduction Mode" 8 | 9 | def plotter() : 10 | NUM_POINTS = 1000 11 | SWITCHING_FREQUENCY = 3 # number of switching cycles to show 12 | VIN = 12.0 13 | VOUT = 3.3 14 | assert VIN > VOUT # just cuz it'll kill my formulas otherwise 15 | IOUT_AVG = 2.5 16 | IOUT_RIPPLE_RATIO = 0.3 # 30% is a common value 17 | 18 | duty_cycle = VOUT / VIN 19 | negative_duty_cycle = 1.0 - duty_cycle 20 | period = 1.0 / SWITCHING_FREQUENCY 21 | on_time = duty_cycle * period 22 | off_time = (1 - duty_cycle) * period 23 | iout_max = IOUT_AVG * (1+(IOUT_RIPPLE_RATIO/2)) 24 | 25 | t = np.linspace(0, 1, NUM_POINTS) 26 | 27 | square_wave = ((t * SWITCHING_FREQUENCY % 1) < (duty_cycle)).astype(float) 28 | v_sw = VIN * square_wave 29 | 30 | triangle_wave = np.mod(t * SWITCHING_FREQUENCY, 1) 31 | triangle_wave = np.where( 32 | triangle_wave < duty_cycle, 33 | 2 * triangle_wave / duty_cycle, # Rising part 34 | 2 * (1 - triangle_wave) / (1 - duty_cycle) # Falling part 35 | ) 36 | i_l = (IOUT_RIPPLE_RATIO / 2) * triangle_wave + (IOUT_AVG - (IOUT_RIPPLE_RATIO / 2)) 37 | 38 | fig, ax = plt.subplots(2, 1, figsize=(8, 6), sharex=True) 39 | 40 | ax[0].plot(t, v_sw, label="Switch Node Voltage (V_sw)") 41 | ax[0].axhline(y=VIN, color='r', linestyle='--', label=f"Input Voltage (V_in)") 42 | ax[0].axhline(y=VOUT, color='r', linestyle='--', label=f"Output Voltage (V_out)") 43 | ax[0].set_title(f"{TITLE}") 44 | ax[0].set_ylim(0, VIN*1.1) 45 | ax[0].set_xlabel("Time (sec)") 46 | ax[0].set_ylabel("Voltage") 47 | ax[0].legend() 48 | 49 | ax[1].plot(t, i_l, label="Load Current (I_l)") 50 | ax[1].axhline(y=IOUT_AVG, color='r', linestyle='--', label=f"Average Load Current (I_out_avg)") 51 | ax[1].set_ylim(0, iout_max*1.1) 52 | ax[1].set_xlabel("Time") 53 | ax[1].set_ylabel("Current") 54 | ax[1].legend() 55 | 56 | if __name__ == "__main__": 57 | parser = argparse.ArgumentParser(f"{TITLE} Plotter") 58 | parser.add_argument("--output_path", help="Path to save image to") 59 | 60 | args = parser.parse_args() 61 | 62 | plotter() 63 | 64 | if args.output_path is not None: 65 | plt.savefig(args.output_path) 66 | else: 67 | plt.show() 68 | 69 | # fmt: on 70 | -------------------------------------------------------------------------------- /src/diagram_generator/buck_dcm.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import matplotlib.pyplot as plt 3 | import argparse 4 | from math import sqrt 5 | 6 | # fmt: off 7 | 8 | TITLE = "Buck Converter in Discontinuous Conduction Mode" 9 | 10 | def plotter() : 11 | VIN = 12 # Input voltage (V) 12 | VOUT = 3.3 # vout 13 | i_out_max = 0.2 14 | D = 0.4 # Duty cycle 15 | T_s = 10e-6 # Switching period (s) 16 | f_sw = 1.0 / T_s 17 | L = 100e-6 # Inductance (H) 18 | C_parasitic = 100e-12 # Parasitic capacitance at the switch node (F) 19 | k = 1000 20 | R_damping = 20*k # Effective damping resistance (Ohm) 21 | f_res = 1 / (2 * np.pi * np.sqrt(L * C_parasitic)) # Resonant frequency (Hz) 22 | f_res = f_res / 2 23 | omega_res = 2 * np.pi * f_res # Angular resonant frequency 24 | t_on = (D - 0.15) * T_s # Switch ON time 25 | t_off = (1 - D - 0.45) * T_s # Switch OFF time before resonance 26 | 27 | NUM_PTS = 5000 28 | 29 | t = np.linspace(0, 3 * T_s, NUM_PTS) 30 | 31 | v_sw = np.zeros_like(t) 32 | i_l = np.zeros_like(t) 33 | i_l_total = 0 34 | for i, t_i in enumerate(t): 35 | cycle_pos = t_i % T_s # Time within each cycle 36 | if cycle_pos < t_on : # Switch ON 37 | v_sw[i] = VIN 38 | i_l[i] = (cycle_pos / t_on) * i_out_max 39 | elif cycle_pos < t_on + t_off: # Diode conduction 40 | v_sw[i] = 0 41 | i_l[i] = i_out_max - ((cycle_pos - t_on) / t_off) * i_out_max 42 | else: # Resonance after diode conduction ends 43 | t_res = cycle_pos - (t_on + t_off) 44 | v_sw[i] = (VOUT) * np.cos(omega_res * t_res - (np.pi)) * np.exp(-t_res / (R_damping * C_parasitic)) + (VOUT) 45 | i_l[i] = 0 46 | i_l_total += i_l[i] 47 | i_out_avg = i_l_total / NUM_PTS 48 | 49 | fig, ax = plt.subplots(2, 1, figsize=(8, 6), sharex=True) 50 | 51 | ax[0].plot(t, v_sw, label="Switch Node Voltage (V_sw)") 52 | ax[0].axhline(y=VIN, color='g', linestyle='--', label=f"Input Voltage (V_in)") 53 | ax[0].axhline(y=VOUT, color='r', linestyle='--', label=f"Output Voltage (V_out)") 54 | ax[0].axhline(y=VOUT*2, color='y', linestyle='--', label=f"V_out*2") 55 | ax[0].set_title(f"{TITLE}") 56 | ax[0].set_ylim(0, VIN*1.1) 57 | ax[0].set_xlabel("Time (sec)") 58 | ax[0].set_ylabel("Voltage (V)") 59 | ax[0].legend() 60 | 61 | ax[1].plot(t, i_l, label="Load Current (I_l)") 62 | ax[1].axhline(y=i_out_avg, color='r', linestyle='--', label=f"Avg Output Current (I_out_avg)") 63 | ax[1].axhline(y=i_out_max, color='y', linestyle='--', label=f"Max Output Current (I_out_max)") 64 | ax[1].set_ylim(0, i_out_max*1.1) 65 | ax[1].set_xlabel("Time (sec)") 66 | ax[1].set_ylabel("Current (A)") 67 | ax[1].legend() 68 | 69 | if __name__ == "__main__": 70 | parser = argparse.ArgumentParser(f"{TITLE} Plotter") 71 | parser.add_argument("--output_path", help="Path to save image to") 72 | 73 | args = parser.parse_args() 74 | 75 | plotter() 76 | 77 | if args.output_path is not None: 78 | plt.savefig(args.output_path) 79 | else: 80 | plt.show() 81 | 82 | # fmt: on 83 | -------------------------------------------------------------------------------- /src/diagram_generator/hpf_bode_plot.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import matplotlib.pyplot as plt 3 | import argparse 4 | import control 5 | 6 | TITLE = "Frequency vs Gain of High Pass Filter" 7 | 8 | 9 | def plotter(): 10 | w0 = 1 11 | sys = control.TransferFunction([1 / w0, 0], [1 / w0, 1]) 12 | mag, phase, omega = control.frequency_response(sys, Hz=True) 13 | 14 | plt.figure(figsize=(10, 6)) 15 | 16 | # only plot the magnitude plot 17 | plt.semilogx(omega, mag, label="Vout/Vin Gain of HPF") 18 | 19 | plt.title(f"{TITLE}") 20 | plt.xlabel("") 21 | plt.ylabel("") 22 | plt.legend() 23 | plt.grid(True) 24 | 25 | 26 | if __name__ == "__main__": 27 | parser = argparse.ArgumentParser(f"{TITLE} Plotter") 28 | parser.add_argument("--output_path", help="Path to save image to") 29 | 30 | args = parser.parse_args() 31 | 32 | plotter() 33 | 34 | if args.output_path is not None: 35 | plt.savefig(args.output_path) 36 | else: 37 | plt.show() 38 | -------------------------------------------------------------------------------- /src/diagram_generator/i2c.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import matplotlib.pyplot as plt 3 | import argparse 4 | 5 | TITLE = "I2C SCL" 6 | 7 | 8 | def plotter(): 9 | kilo = 1e3 10 | nano = 1e-9 11 | pico = 1e-12 12 | 13 | # trying to be realistic with values 14 | freq = 400 * kilo 15 | tau = 2.2 * kilo * 47 * pico 16 | v_ll = 3.3 17 | 18 | period = 1.0 / float(freq) 19 | half_period = period / 2 20 | 21 | number_of_plotted_points = 5000 22 | cycles_shown = 3 23 | t = np.linspace(0, cycles_shown * period, number_of_plotted_points) 24 | 25 | scl = np.zeros_like(t) 26 | for i, t_i in enumerate(t): 27 | cycle_pos = t_i % period # Time within each cycle 28 | if cycle_pos < half_period: 29 | scl[i] = 0 30 | else: 31 | scl[i] = v_ll * (1 - np.exp((-1 * (cycle_pos - half_period)) / tau)) 32 | 33 | plt.figure(figsize=(10, 6)) 34 | plt.title(f"{TITLE}") 35 | plt.plot(t, scl, label="SCL") 36 | plt.xlabel("Time (sec)") 37 | # plt.xlim(0, number_of_plotted_points) 38 | plt.ylabel("Voltage (V)") 39 | plt.legend() 40 | plt.grid(True) 41 | 42 | 43 | if __name__ == "__main__": 44 | parser = argparse.ArgumentParser(f"{TITLE} Plotter") 45 | parser.add_argument("--output_path", help="Path to save image to") 46 | 47 | args = parser.parse_args() 48 | 49 | plotter() 50 | 51 | if args.output_path is not None: 52 | plt.savefig(args.output_path) 53 | else: 54 | plt.show() 55 | -------------------------------------------------------------------------------- /src/diagram_generator/i2c_comparison.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import matplotlib.pyplot as plt 3 | import argparse 4 | 5 | TITLE = "I2C SCL with Varying Pullup Resistance" 6 | 7 | 8 | def plotter(): 9 | kilo = 1e3 10 | pico = 1e-12 11 | 12 | freq = 400 * kilo 13 | v_ll = 3.3 14 | 15 | tau = 2.2 * kilo * 47 * pico 16 | tau_higher = 4.7 * kilo * 47 * pico 17 | tau_lower = 470 * 47 * pico 18 | 19 | period = 1.0 / float(freq) 20 | half_period = period / 2 21 | 22 | number_of_plotted_points = 5000 23 | cycles_shown = 3 24 | t = np.linspace(0, cycles_shown * period, number_of_plotted_points) 25 | 26 | scl = np.zeros_like(t) 27 | scl_higher = np.zeros_like(t) 28 | scl_lower = np.zeros_like(t) 29 | for i, t_i in enumerate(t): 30 | cycle_pos = t_i % period # Time within each cycle 31 | if cycle_pos < half_period: 32 | scl[i] = 0 33 | scl_higher[i] = 0 34 | scl_lower[i] = 0 35 | else: 36 | scl[i] = v_ll * (1 - np.exp((-1 * (cycle_pos - half_period)) / tau)) 37 | scl_higher[i] = v_ll * ( 38 | 1 - np.exp((-1 * (cycle_pos - half_period)) / tau_higher) 39 | ) 40 | scl_lower[i] = v_ll * ( 41 | 1 - np.exp((-1 * (cycle_pos - half_period)) / tau_lower) 42 | ) 43 | 44 | plt.figure(figsize=(10, 6)) 45 | plt.title(f"{TITLE}") 46 | plt.plot(t, scl, label="SCL Nominal") 47 | plt.plot(t, scl_higher, label="SCL with Increased Pull-up Value") 48 | plt.plot(t, scl_lower, label="SCL with Decreased Pull-up Value") 49 | plt.xlabel("Time (sec)") 50 | # plt.xlim(0, number_of_plotted_points) 51 | plt.ylabel("Voltage (V)") 52 | plt.legend() 53 | plt.grid(True) 54 | 55 | 56 | if __name__ == "__main__": 57 | parser = argparse.ArgumentParser(f"{TITLE} Plotter") 58 | parser.add_argument("--output_path", help="Path to save image to") 59 | 60 | args = parser.parse_args() 61 | 62 | plotter() 63 | 64 | if args.output_path is not None: 65 | plt.savefig(args.output_path) 66 | else: 67 | plt.show() 68 | -------------------------------------------------------------------------------- /src/diagram_generator/pid_discretization.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import matplotlib.pyplot as plt 3 | import argparse 4 | 5 | NUM_POINTS = 10 6 | 7 | 8 | def plot_sine_backward_derivative_and_sum(NUM_POINTS=20): 9 | # Time range 10 | T = 2 * np.pi # One full sine wave period 11 | dt = T / NUM_POINTS # Time step based on the number of points 12 | t = np.linspace(0, T, 1000) # Continuous time for the sine wave 13 | 14 | # Continuous sine wave 15 | y_cont = np.sin(t) 16 | 17 | # Discretized points for sine wave 18 | t_discrete = np.linspace(0, T, NUM_POINTS + 1) 19 | y_discrete = np.sin(t_discrete) 20 | 21 | # Plotting 22 | plt.figure(figsize=(10, 6)) 23 | 24 | # Continuous sine wave 25 | plt.plot(t, y_cont, color="black", label="Continuous Function") 26 | 27 | # Backward Euler Derivative Lines 28 | for i in range(1, NUM_POINTS + 1): 29 | plt.plot( 30 | [t_discrete[i - 1], t_discrete[i]], 31 | [y_discrete[i - 1], y_discrete[i]], 32 | color="magenta", 33 | linestyle="-", 34 | linewidth=2, 35 | zorder=5, 36 | ) 37 | plt.scatter( 38 | [t_discrete[i - 1], t_discrete[i]], 39 | [y_discrete[i - 1], y_discrete[i]], 40 | color="magenta", 41 | zorder=5, 42 | ) 43 | plt.plot( 44 | [], [], color="magenta", linewidth=2, label="Backward Euler (Derivative)" 45 | ) # Legend entry 46 | 47 | # Right-Hand Riemann Sum Rectangles 48 | for i in range(1, NUM_POINTS + 1): 49 | plt.plot( 50 | [t_discrete[i], t_discrete[i]], 51 | [0, y_discrete[i]], 52 | color="yellow", 53 | linewidth=2, 54 | zorder=4, 55 | ) 56 | plt.plot( 57 | [t_discrete[i - 1], t_discrete[i]], 58 | [y_discrete[i], y_discrete[i]], 59 | color="yellow", 60 | linewidth=2, 61 | zorder=4, 62 | ) 63 | plt.plot( 64 | [t_discrete[i - 1], t_discrete[i - 1]], 65 | [0, y_discrete[i]], 66 | color="yellow", 67 | linewidth=2, 68 | zorder=4, 69 | ) 70 | plt.plot( 71 | [], [], color="yellow", linewidth=2, label="Backward Euler (Sum)" 72 | ) # Legend entry 73 | 74 | # Discrete points 75 | plt.scatter(t_discrete, y_discrete, color="cyan", zorder=6, label="Discrete Points") 76 | 77 | # Labels and styling 78 | plt.title( 79 | "Sine Wave with Raw Backward Derivative and Backward Euler Sum Calculations" 80 | ) 81 | plt.xlabel("Time (t)") 82 | plt.ylabel("Value") 83 | plt.legend() 84 | plt.grid(True) 85 | 86 | 87 | if __name__ == "__main__": 88 | parser = argparse.ArgumentParser("PID Discretization Plotter") 89 | parser.add_argument("--output_path", help="Path to save image to") 90 | 91 | args = parser.parse_args() 92 | 93 | plot_sine_backward_derivative_and_sum(NUM_POINTS) 94 | 95 | if args.output_path is not None: 96 | plt.savefig(args.output_path) 97 | else: 98 | plt.show() 99 | -------------------------------------------------------------------------------- /src/diagram_generator/pid_discretization_derivative.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import matplotlib.pyplot as plt 3 | import argparse 4 | 5 | NUM_POINTS = 4 6 | 7 | 8 | def plot_sine_and_derivative_comparison(NUM_POINTS=20): 9 | # Time range 10 | T_start = 0.0 * np.pi 11 | T_end = 1.0 * np.pi 12 | dt = T_end / NUM_POINTS # Time step based on the number of points 13 | t = np.linspace(T_start, T_end, 1000) # Continuous time for the sine wave 14 | 15 | # Continuous sine wave and its derivative 16 | y_cont = np.sin(t) 17 | dy_cont = np.cos(t) 18 | 19 | # Discretized points for sine wave 20 | t_discrete = np.linspace(T_start, T_end, NUM_POINTS + 1) 21 | y_discrete = np.sin(t_discrete) 22 | 23 | # Compute backward Euler derivative 24 | dy_discrete = np.diff(y_discrete) / dt # Discretized derivative approximation 25 | t_derivative = t_discrete[:-1] # Time points for the derivative (one less) 26 | 27 | # Create subplots 28 | fig, axes = plt.subplots(2, 1, figsize=(10, 6)) 29 | 30 | # Plot continuous sine wave and backward Euler approximation 31 | axes[0].plot(t, y_cont, color="black", label="Continuous Function") 32 | for i in range(1, NUM_POINTS + 1): 33 | # Plot backward Euler approximation 34 | axes[0].plot( 35 | [t_discrete[i - 1], t_discrete[i]], 36 | [y_discrete[i - 1], y_discrete[i]], 37 | color="magenta", 38 | linestyle="-", 39 | linewidth=2, 40 | zorder=5, 41 | ) 42 | axes[0].scatter( 43 | [t_discrete[i - 1], t_discrete[i]], 44 | [y_discrete[i - 1], y_discrete[i]], 45 | color="magenta", 46 | zorder=6, 47 | ) 48 | 49 | # Calculate rise and run 50 | rise = y_discrete[i] - y_discrete[i - 1] 51 | run = t_discrete[i] - t_discrete[i - 1] 52 | 53 | # Offset for arrows (small margin) 54 | arrow_offset = 0.0 55 | 56 | # Plot rise marker 57 | axes[0].arrow( 58 | t_discrete[i] + arrow_offset, 59 | y_discrete[i - 1], # Offset arrow slightly 60 | 0, 61 | rise, # Delta for the arrow 62 | color="green", 63 | alpha=0.7, 64 | head_width=0.03, 65 | head_length=0.05, 66 | width=0.005, 67 | length_includes_head=True, 68 | zorder=4, 69 | ) 70 | 71 | # Plot run marker 72 | axes[0].arrow( 73 | t_discrete[i - 1], 74 | y_discrete[i - 1] - arrow_offset, # Offset arrow slightly 75 | run, 76 | 0, # Delta for the arrow 77 | color="blue", 78 | alpha=0.7, 79 | head_width=0.03, 80 | head_length=0.05, 81 | width=0.005, 82 | length_includes_head=True, 83 | zorder=4, 84 | ) 85 | axes[0].plot([], [], color="magenta", linewidth=2, label="Discretized Function") 86 | axes[0].plot([], [], color="green", label="Rise") 87 | axes[0].plot([], [], color="blue", label="Run") 88 | axes[0].scatter( 89 | t_discrete, y_discrete, color="red", zorder=7, label="Discrete Points" 90 | ) 91 | axes[0].set_title("Sine Wave with Backward Derivative Approximation") 92 | axes[0].set_xlabel("Time (t)") 93 | axes[0].set_ylabel("Value") 94 | axes[0].legend() 95 | axes[0].grid(True) 96 | 97 | # Plot actual derivative and discretized derivative 98 | axes[1].plot(t, dy_cont, color="black", label="Continuous Derivative") 99 | axes[1].scatter( 100 | t_derivative, 101 | dy_discrete, 102 | color="magenta", 103 | label="Discretized Derivative", 104 | zorder=5, 105 | ) 106 | axes[1].plot(t_derivative, dy_discrete, color="magenta", linestyle="--", zorder=5) 107 | axes[1].set_title("Derivative Comparison") 108 | axes[1].set_xlabel("Time (t)") 109 | axes[1].set_ylabel("Derivative") 110 | axes[1].legend() 111 | axes[1].grid(True) 112 | 113 | # Adjust layout 114 | plt.tight_layout() 115 | 116 | 117 | if __name__ == "__main__": 118 | parser = argparse.ArgumentParser("PID Discretization Plotter") 119 | parser.add_argument("--output_path", help="Path to save image to") 120 | 121 | args = parser.parse_args() 122 | 123 | plot_sine_and_derivative_comparison(NUM_POINTS) 124 | 125 | if args.output_path is not None: 126 | plt.savefig(args.output_path) 127 | else: 128 | plt.show() 129 | -------------------------------------------------------------------------------- /src/diagram_generator/pid_discretization_integral.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import matplotlib.pyplot as plt 3 | import argparse 4 | 5 | NUM_POINTS = 20 6 | 7 | 8 | def plot_sine_backward_derivative_and_sum(NUM_POINTS=20): 9 | # Time range 10 | T = 0.5 * np.pi # One full sine wave period 11 | dt = T / NUM_POINTS # Time step based on the number of points 12 | t = np.linspace(0, T, 1000) # Continuous time for the sine wave 13 | 14 | # Continuous sine wave 15 | y_cont = np.sin(t) 16 | 17 | # Discretized points for sine wave 18 | t_discrete = np.linspace(0, T, NUM_POINTS + 1) 19 | y_discrete = np.sin(t_discrete) 20 | 21 | # Plotting 22 | plt.figure(figsize=(10, 6)) 23 | 24 | # Continuous sine wave 25 | plt.plot(t, y_cont, color="black", label="Continuous Function") 26 | 27 | # Right-Hand Riemann Sum Rectangles 28 | for i in range(1, NUM_POINTS + 1): 29 | plt.plot( 30 | [t_discrete[i], t_discrete[i]], 31 | [0, y_discrete[i]], 32 | color="blue", 33 | linewidth=2, 34 | zorder=4, 35 | ) 36 | plt.plot( 37 | [t_discrete[i - 1], t_discrete[i]], 38 | [y_discrete[i], y_discrete[i]], 39 | color="blue", 40 | linewidth=2, 41 | zorder=4, 42 | ) 43 | plt.plot( 44 | [t_discrete[i - 1], t_discrete[i - 1]], 45 | [0, y_discrete[i]], 46 | color="blue", 47 | linewidth=2, 48 | zorder=4, 49 | ), 50 | # Plot the bottom line explicitly 51 | plt.plot( 52 | [t_discrete[i - 1], t_discrete[i]], 53 | [0, 0], 54 | color="blue", 55 | linewidth=2, 56 | zorder=4, 57 | ) 58 | plt.plot( 59 | [], [], color="blue", linewidth=2, label="Backward Euler (Sum)" 60 | ) # Legend entry 61 | 62 | # Discrete points 63 | plt.scatter(t_discrete, y_discrete, color="red", zorder=6, label="Discrete Points") 64 | 65 | # Labels and styling 66 | plt.title("Sine Wave with Overlayed Backward Euler Sum Calculations") 67 | plt.xlabel("Time (t)") 68 | plt.ylabel("Value") 69 | plt.legend() 70 | plt.grid(True) 71 | 72 | 73 | if __name__ == "__main__": 74 | parser = argparse.ArgumentParser("PID Discretization Plotter") 75 | parser.add_argument("--output_path", help="Path to save image to") 76 | 77 | args = parser.parse_args() 78 | 79 | plot_sine_backward_derivative_and_sum(NUM_POINTS) 80 | 81 | if args.output_path is not None: 82 | plt.savefig(args.output_path) 83 | else: 84 | plt.show() 85 | -------------------------------------------------------------------------------- /src/diagram_generator/plot_template.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import matplotlib.pyplot as plt 3 | import argparse 4 | 5 | TITLE = "Unknown Plot Title" 6 | 7 | 8 | def plotter(): 9 | 10 | # Draw ! 11 | 12 | plt.figure(figsize=(10, 6)) 13 | 14 | # Draw ! 15 | 16 | plt.title(f"{TITLE}") 17 | plt.xlabel("") 18 | plt.ylabel("") 19 | plt.legend() 20 | plt.grid(True) 21 | 22 | 23 | if __name__ == "__main__": 24 | parser = argparse.ArgumentParser(f"{TITLE} Plotter") 25 | parser.add_argument("--output_path", help="Path to save image to") 26 | 27 | args = parser.parse_args() 28 | 29 | plotter() 30 | 31 | if args.output_path is not None: 32 | plt.savefig(args.output_path) 33 | else: 34 | plt.show() 35 | -------------------------------------------------------------------------------- /src/diagram_generator/priority_inheritance_input.json: -------------------------------------------------------------------------------- 1 | { 2 | "task_data": { 3 | "task_high": { 4 | "name": "High Priority Task\nReady at t=2", 5 | "active": [ 6 | 0, 7 | 1, 8 | 0, 9 | 1, 10 | 0, 11 | 0 12 | ] 13 | }, 14 | "task_med": { 15 | "name": "Medium Priority Task\nReady at t=2", 16 | "active": [ 17 | 0, 18 | 0, 19 | 0, 20 | 0, 21 | 1, 22 | 1 23 | ] 24 | }, 25 | "task_low": { 26 | "name": "Low Priority Task\nReady at t=1", 27 | "active": [ 28 | 1, 29 | 0, 30 | 1, 31 | 0, 32 | 0, 33 | 0 34 | ] 35 | } 36 | }, 37 | "title": "Task Schedule with Priority Inheritance", 38 | "annotations": [ 39 | { 40 | "task": "task_low", 41 | "time": 0, 42 | "position": "start", 43 | "text": "Mutex Lock" 44 | }, 45 | { 46 | "task": "task_high", 47 | "time": 1, 48 | "position": "start", 49 | "text": "Mutex Lock, Fails" 50 | }, 51 | { 52 | "task": "task_low", 53 | "time": 2, 54 | "position": "start", 55 | "text": "Inherits High Priority\nReleases Mutex" 56 | }, 57 | { 58 | "task": "task_high", 59 | "time": 3, 60 | "position": "end", 61 | "text": "Resumes Execution" 62 | }, 63 | { 64 | "task": "task_med", 65 | "time": 4, 66 | "position": "start", 67 | "text": "Starts execution" 68 | } 69 | ] 70 | } 71 | -------------------------------------------------------------------------------- /src/diagram_generator/priority_inversion_input.json: -------------------------------------------------------------------------------- 1 | { 2 | "task_data": { 3 | "task_high": { 4 | "name": "High Priority Task\nReady at t=2", 5 | "active": [ 6 | 0, 7 | 1, 8 | 0, 9 | 0, 10 | 0 11 | ] 12 | }, 13 | "task_med": { 14 | "name": "Medium Priority Task\nReady at t=2", 15 | "active": [ 16 | 0, 17 | 0, 18 | 1, 19 | 1, 20 | 1 21 | ] 22 | }, 23 | "task_low": { 24 | "name": "Low Priority Task\nReady at t=1", 25 | "active": [ 26 | 1, 27 | 0, 28 | 0, 29 | 0, 30 | 0 31 | ] 32 | } 33 | }, 34 | "title": "Task Schedule with Priority Inversion", 35 | "annotations": [ 36 | { 37 | "task": "task_high", 38 | "time": 1, 39 | "position": "start", 40 | "text": "Mutex Lock, Fails" 41 | }, 42 | { 43 | "task": "task_low", 44 | "time": 0, 45 | "position": "start", 46 | "text": "Mutex Lock" 47 | }, 48 | { 49 | "task": "task_med", 50 | "time": 2, 51 | "position": "start", 52 | "text": "Next Free Task Runs" 53 | }, 54 | { 55 | "task": "task_med", 56 | "time": 4, 57 | "position": "end", 58 | "text": "Never forced to yield as no\nready higher priority task\nRuns to completion" 59 | } 60 | ] 61 | } -------------------------------------------------------------------------------- /src/diagram_generator/rtos_task_diagram.py: -------------------------------------------------------------------------------- 1 | import matplotlib.pyplot as plt 2 | import matplotlib.patches as patches 3 | import json 4 | import argparse 5 | 6 | 7 | def plot_task_schedule(task_data, title, annotations=None): 8 | """ 9 | Plots a Gantt-like task schedule with optional annotations. 10 | 11 | Parameters: 12 | - task_data (dict): Dictionary containing task names and their active status. 13 | - title (str): Title of the plot. 14 | - annotations (list): List of dictionaries specifying annotations. 15 | """ 16 | fig, ax = plt.subplots(figsize=(10, 5)) 17 | 18 | # Y-axis task labels 19 | task_names = list(task_data.keys()) 20 | display_names = [task_info["name"] for task_info in task_data.values()] 21 | num_tasks = len(display_names) 22 | ax.set_yticks(range(num_tasks)) 23 | ax.set_yticklabels(display_names) 24 | 25 | # Time axis (assuming uniform time steps) 26 | max_time = max(len(task["active"]) for task in task_data.values()) 27 | ax.set_xticks(range(max_time)) 28 | ax.set_xticklabels(range(1, max_time + 1)) # Time steps start from 1 29 | 30 | # Plot each task as horizontal blocks where active == 1 31 | box_height = 0.4 # Reduced height for rectangles 32 | for i, (task_name, task_info) in enumerate(task_data.items()): 33 | active_states = task_info["active"] 34 | for time_idx, active in enumerate(active_states): 35 | if active == 1: 36 | rect = patches.Rectangle( 37 | (time_idx, i - box_height / 2), # Center the rectangle vertically 38 | 1, # Width of the rectangle 39 | box_height, # Height of the rectangle 40 | facecolor=None, 41 | edgecolor="hotpink", 42 | lw=1.5, 43 | hatch="////", 44 | ) 45 | ax.add_patch(rect) 46 | 47 | # Add annotations if provided 48 | if annotations: 49 | for annotation in annotations: 50 | add_annotation(ax, annotation, task_names, box_height) 51 | 52 | # Beautify the plot 53 | ax.set_xlim(0, max_time) 54 | ax.set_ylim(-0.5, num_tasks - 0.5) 55 | ax.invert_yaxis() # Invert Y-axis to align with input order 56 | ax.set_xlabel("Time") 57 | ax.set_ylabel("Tasks") 58 | ax.set_title(title) 59 | ax.grid(axis="x", linestyle="--", alpha=0.5) 60 | 61 | # Add a legend for the hatched rectangle 62 | legend_patch = patches.Patch( 63 | facecolor="none", edgecolor="hotpink", hatch="////", label="Task Running" 64 | ) 65 | ax.legend(handles=[legend_patch], loc="upper right", fontsize=10, frameon=True) 66 | 67 | plt.tight_layout() 68 | 69 | 70 | def add_annotation(ax, annotation, task_names, box_height): 71 | """ 72 | Adds an annotation to the plot based on task and time position. 73 | 74 | Parameters: 75 | - ax: Matplotlib Axes object to add annotation to. 76 | - annotation (dict): Annotation schema. 77 | - task_names (list): List of task identifiers to find task index. 78 | - box_height (float): Height of the rectangle to adjust text alignment. 79 | """ 80 | task_index = task_names.index(annotation["task"]) 81 | time_index = annotation["time"] 82 | position = annotation["position"] 83 | text = annotation["text"] 84 | 85 | # Adjust alignment based on position (start or end) 86 | if position == "start": 87 | ha_align = "left" 88 | x_pos = time_index + 0.05 89 | else: # 'end' 90 | ha_align = "right" 91 | x_pos = time_index + 0.95 92 | 93 | y_pos = task_index 94 | 95 | ax.text( 96 | x_pos, 97 | y_pos, 98 | text, 99 | ha=ha_align, 100 | va="center", 101 | color="black", 102 | fontsize=8, 103 | fontweight="normal", 104 | bbox=dict(facecolor="white", edgecolor="none", boxstyle="round,pad=0.3"), 105 | ) 106 | 107 | 108 | def main(): 109 | """Main function to parse arguments and plot the task schedule.""" 110 | parser = argparse.ArgumentParser(description="Generate an RTOS task diagram.") 111 | parser.add_argument( 112 | "input_data_path", 113 | type=str, 114 | help="Path to JSON file containing task data.", 115 | ) 116 | parser.add_argument("--output_path", type=str, help="Path to save the plot.") 117 | 118 | args = parser.parse_args() 119 | 120 | # Load task data from JSON file 121 | with open(args.input_data_path, "r") as file: 122 | input_data = json.load(file) 123 | 124 | # Run the function 125 | plot_task_schedule( 126 | input_data["task_data"], input_data["title"], input_data.get("annotations") 127 | ) 128 | 129 | # Save or display the plot 130 | if args.output_path: 131 | plt.savefig(args.output_path) 132 | else: 133 | plt.show() 134 | 135 | 136 | if __name__ == "__main__": 137 | main() 138 | -------------------------------------------------------------------------------- /src/diagram_generator/scope-trigger.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import matplotlib.pyplot as plt 3 | import argparse 4 | 5 | TITLE = "Scope Triggering Waveform" 6 | 7 | 8 | def plotter(): 9 | plt.figure(figsize=(16, 11)) 10 | 11 | num_sin_wave_completions = 3 12 | 13 | trigger_time_offset_rad = 1 / 6 * np.pi 14 | 15 | t = np.linspace(-3 / 4 * np.pi, 2 * np.pi * num_sin_wave_completions, num=1000) 16 | 17 | # create 2 subplots, one for showing un-triggered waveform (bunch of sincewaves and messy), and one for triggered waveform 18 | plt.subplot(2, 1, 1) 19 | 20 | num_overlapping_waves = 6 21 | for i in range(num_overlapping_waves): 22 | phase_delay_const_rad = i * np.pi / 6 23 | sin_wave = np.sin(t + phase_delay_const_rad + trigger_time_offset_rad) 24 | plt.plot(t, sin_wave) 25 | 26 | plt.xlabel("Time (us)") 27 | plt.ylabel("Amplitude") 28 | plt.title("Un-triggered Waveform (Measurements Appear Overlapping)") 29 | plt.grid() 30 | 31 | plt.subplot(2, 1, 2) 32 | sin_wave = np.sin(t + trigger_time_offset_rad) 33 | plt.plot(t, sin_wave, label="Actual Waveform (Triggered)") 34 | 35 | # draw trigger line 36 | trigger_value = np.sin(trigger_time_offset_rad) 37 | plt.axhline( 38 | y=trigger_value, color="r", linestyle="--", label="Trigger Value (Rising Edge)" 39 | ) 40 | 41 | # draw trigger time at 0.0s 42 | plt.axvline(x=0.0, color="purple", linestyle="--", label="Trigger Time") 43 | 44 | # draw a circle at the trigger valye and trigger time 45 | plt.plot(0.0, trigger_value, "ro") 46 | 47 | plt.xlabel("Time (us)") 48 | plt.ylabel("Amplitude") 49 | plt.title("Triggered Waveform") 50 | plt.grid() 51 | plt.legend() 52 | 53 | 54 | if __name__ == "__main__": 55 | parser = argparse.ArgumentParser(f"{TITLE} Plotter") 56 | parser.add_argument("--output_path", help="Path to save image to") 57 | 58 | args = parser.parse_args() 59 | 60 | plotter() 61 | 62 | if args.output_path is not None: 63 | plt.savefig(args.output_path) 64 | else: 65 | plt.show() 66 | -------------------------------------------------------------------------------- /src/diagram_generator/step_response_plotter.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import matplotlib.pyplot as plt 3 | import argparse 4 | import control 5 | import json 6 | 7 | PRIMARY_COLOUR = "blue" 8 | SECONDARY_COLOUR = "purple" 9 | 10 | 11 | def plotter(t, y, title, y_labels: list, input_signal_label, output_signal_label): 12 | 13 | plt.figure(figsize=(10, 6)) 14 | 15 | t_input = np.linspace(-1, t[-1], 1000) 16 | v_input = [1 if i >= 0 else 0 for i in t_input] 17 | 18 | # Plot input signal on primary y-axis 19 | ax1 = plt.gca() # Primary axis 20 | (input_line,) = ax1.plot( 21 | t_input, v_input, label=input_signal_label, color=PRIMARY_COLOUR 22 | ) 23 | ax1.set_xlabel("Time (s)") 24 | 25 | ax1_color = "black" 26 | 27 | # Check if a second y-axis is needed 28 | if len(y_labels) == 2: 29 | # Plot output signal on secondary y-axis 30 | ax2 = ax1.twinx() # Secondary axis 31 | (output_line,) = ax2.plot( 32 | t, y, label=output_signal_label, color=SECONDARY_COLOUR 33 | ) 34 | ax2.set_ylabel(y_labels[1], color=SECONDARY_COLOUR) 35 | ax2.tick_params(axis="y", labelcolor=SECONDARY_COLOUR) 36 | 37 | ax1_color = PRIMARY_COLOUR 38 | else: 39 | # Plot output signal on primary y-axis if only one label 40 | (output_line,) = ax1.plot( 41 | t, y, label=output_signal_label, color=SECONDARY_COLOUR 42 | ) 43 | 44 | ax1.set_ylabel(y_labels[0], color=ax1_color) 45 | ax1.tick_params(axis="y", labelcolor=ax1_color) 46 | 47 | # Combine legends from both axes 48 | lines = [input_line, output_line] 49 | labels = [line.get_label() for line in lines] 50 | ax1.legend(lines, labels) 51 | 52 | plt.title(title) 53 | plt.grid(True) 54 | 55 | 56 | def get_step_response(numerator, denominator): 57 | sys = control.TransferFunction(numerator, denominator) 58 | t, y = control.step_response(sys) 59 | return t, y 60 | 61 | 62 | if __name__ == "__main__": 63 | parser = argparse.ArgumentParser(f"Step Response Plotter") 64 | parser.add_argument( 65 | "--input_file", 66 | help="Path to input file to use for image generation", 67 | required=True, 68 | ) 69 | parser.add_argument( 70 | "--display_plot", help="Displays the selected plot key" 71 | ) # ex: --display_plot=low_pass_filter 72 | parser.add_argument("--output_path", help="Directory to save images to") 73 | 74 | args = parser.parse_args() 75 | 76 | # Load the input file 77 | with open(args.input_file) as f: 78 | data = json.load(f) 79 | 80 | for key in data: 81 | plt.close("all") 82 | plt.rcdefaults() 83 | 84 | # coefficients are in descending order, ex: [2, 3, 4] -> 2s^2 + 3s + 4 85 | numerator = data[key]["numerator"] 86 | denominator = data[key]["denominator"] 87 | title = data[key]["title"] 88 | y_labels = data[key]["y_label"] 89 | input_signal_label = data[key]["input_signal_label"] 90 | output_signal_label = data[key]["output_signal_label"] 91 | 92 | t, y = get_step_response(numerator, denominator) 93 | 94 | plotter(t, y, title, y_labels, input_signal_label, output_signal_label) 95 | 96 | if args.output_path is not None: 97 | image_path = f"{args.output_path}/{key}.png" 98 | plt.savefig(image_path) 99 | elif (key == args.display_plot) or (args.display_plot is None): 100 | plt.show() 101 | elif args.display_plot is not None: 102 | print(f"Skipping display for {key}") 103 | -------------------------------------------------------------------------------- /src/diagram_generator/step_responses.json: -------------------------------------------------------------------------------- 1 | { 2 | "series_resistor": { 3 | "title" : "Step Response of a Series Resistor", 4 | "numerator": [1], 5 | "denominator": [1], 6 | "y_label": ["Voltage (V)"], 7 | "input_signal_label": "Step Voltage Input", 8 | "output_signal_label": "Step Response of System Voltage" 9 | }, 10 | "voltage_divider" :{ 11 | "title" : "Step Response of a Voltage Divider", 12 | "numerator": [0.5], 13 | "denominator": [1], 14 | "y_label": ["Voltage (V)"], 15 | "input_signal_label": "Step Voltage Input", 16 | "output_signal_label": "Step Response of System Voltage" 17 | }, 18 | "low_pass_filter": { 19 | "title" : "Step Response of a Low Pass Filter", 20 | "numerator": [1], 21 | "denominator": [1, 1], 22 | "y_label": ["Voltage (V)"], 23 | "input_signal_label": "Step Voltage Input", 24 | "output_signal_label": "Step Response of System Voltage" 25 | }, 26 | "high_pass_filter": { 27 | "title" : "Step Response of a High Pass Filter", 28 | "numerator": [1, 0], 29 | "denominator": [1, 0.1], 30 | "y_label": ["Voltage (V)"], 31 | "input_signal_label": "Step Voltage Input", 32 | "output_signal_label": "Step Response of System Voltage" 33 | }, 34 | "series_cap_with_current_source": { 35 | "title" : "Step Response of a Series Capacitor with a Current Source", 36 | "numerator": [1], 37 | "denominator": [10, 0], 38 | "y_label": ["Current (A)", "Voltage (V)"], 39 | "input_signal_label": "Step Current Input", 40 | "output_signal_label": "Step Response of System Voltage" 41 | }, 42 | "series_inductor_with_shunt_cap": { 43 | "title" : "Step Response of a Series Inductor with Shunt Capacitor", 44 | "numerator": [1], 45 | "denominator": [1, 0, 1], 46 | "y_label": ["Voltage (V)"], 47 | "input_signal_label": "Step Voltage Input", 48 | "output_signal_label": "Step Response of System Voltage" 49 | } 50 | } -------------------------------------------------------------------------------- /src/diagram_generator/wavedrom/can_bus_arbitration.json: -------------------------------------------------------------------------------- 1 | { 2 | "signal": [ 3 | { 4 | "name": "Node A: ID 0x452", 5 | "wave": "xz2..z2zxxxx" , 6 | }, 7 | { 8 | "name": "Node A in Binary", 9 | "wave": "x3888389xxxx", 10 | "node": ".......c....", 11 | "data": ["1", "0", "0", "0", "1", "0", "1"] 12 | }, 13 | { 14 | "name": "Node B: ID 0x442", 15 | "wave": "xz2..z2...z2", 16 | }, 17 | { 18 | "name": "Node B in Binary", 19 | "wave": "x38883878838", 20 | "data": ["1", "0", "0", "0", "1", "0", "0", "0", "0", "1", "0"] 21 | }, 22 | { 23 | "name": "Bus Waveform", 24 | "wave": "xz2..z2...z2", 25 | }, 26 | { 27 | "name": "Bus Waveform in Binary", 28 | "wave": "x38883878838", 29 | "node": "........d...", 30 | "data": ["1", "0", "0", "0", "1", "0", "0", "0", "0", "1", "0"] 31 | }, 32 | ], 33 | "edge": [ 34 | "c->d Node A Loses Arbitration",] 35 | } 36 | -------------------------------------------------------------------------------- /src/diagram_generator/wavedrom/can_dominant_recessive.json: -------------------------------------------------------------------------------- 1 | { 2 | "signal": [ 3 | { 4 | "name": "CAN H", 5 | "wave": "z1" , 6 | }, 7 | { 8 | "name": "CAN L", 9 | "wave": "z0" , 10 | }, 11 | { 12 | "name": "Binary", 13 | "wave": "38", 14 | "node": "........d...", 15 | "data": ["1 (recessive)", "0 (dominant)"] 16 | }, 17 | { 18 | "name": "Equivalent Bus Waveform", 19 | "wave": "z2" , 20 | }, 21 | ], 22 | "config" : { 23 | "hscale": 3 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/diagram_generator/wavedrom/diff_digital_signal.json: -------------------------------------------------------------------------------- 1 | { 2 | "signal": [ 3 | { 4 | "name": "DIFF_P (+)", 5 | "wave": "x101..010.1x" 6 | }, 7 | { 8 | "name": "DIFF_N (-)", 9 | "wave": "x010..101.0x" 10 | }, 11 | { 12 | "name": "Output (Binary)", 13 | "wave": "x7677767667x", 14 | "data": ["0", "1", "0", "0", "0", "1", "0", "1", "1", "0"] 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /src/diagram_generator/wavedrom/i2c_data_format.json: -------------------------------------------------------------------------------- 1 | { 2 | "signal": [ 3 | { 4 | "name": "SDA", 5 | "wave": "x8.3..54...74...78.x", 6 | "data": ["START", "7-bit Addr", "R/W", "8-bit Data","ACK", "8-bit Data","ACK", "STOP"] 7 | }, 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /src/diagram_generator/wavedrom/packet_parsing.json: -------------------------------------------------------------------------------- 1 | { 2 | "signal": [ 3 | { 4 | "wave": "z34.4.6.6.6.6.7z", 5 | "data": [ 6 | "0x55", 7 | "Temp MSB + 32", 8 | "Temp LSB (Fractional)", 9 | "Pressure MSB", 10 | "Pressure Byte 2", 11 | "Pressure Byte 3", 12 | "Pressure LSB", 13 | "Checksum" 14 | ] 15 | }, 16 | ], 17 | "config": { 18 | "hscale": 2, 19 | }, 20 | } 21 | -------------------------------------------------------------------------------- /src/diagram_generator/wavedrom/parity.json: -------------------------------------------------------------------------------- 1 | { 2 | "signal": [ 3 | { 4 | "name": "Bitstream - No Parity", 5 | "wave": "x101.0.1.xx" 6 | }, 7 | { 8 | "name": "Binary - No Parity", 9 | "wave": "x33333333xx", 10 | "data": ["1", "0", "1", "1", "0", "0", "1", "1"] 11 | }, 12 | { 13 | "name": "Bitstream - Odd Parity", 14 | "wave": "x101.0.1.0x" 15 | }, 16 | { 17 | "name": "Binary - Odd Parity", 18 | "wave": "x333333337x", 19 | "data": ["1", "0", "1", "1", "0", "0", "1", "1", "0"] 20 | }, 21 | { 22 | "name": "Bitstream - Even Parity", 23 | "wave": "x101.0.1..x" 24 | }, 25 | { 26 | "name": "Binary - Even Parity", 27 | "wave": "x333333337x", 28 | "data": ["1", "0", "1", "1", "0", "0", "1", "1", "1"] 29 | }, 30 | { 31 | "name": "Bitstream - Even Parity (Incorrect)", 32 | "wave": "x1010..1..x" 33 | }, 34 | { 35 | "name": "Binary - Even Parity (Incorrect)", 36 | "wave": "x333433339x", 37 | "data": ["1", "0", "1", "0", "0", "0", "1", "1", "1"] 38 | }, 39 | ] 40 | } 41 | -------------------------------------------------------------------------------- /src/diagram_generator/wavedrom/pwm.json: -------------------------------------------------------------------------------- 1 | { 2 | "signal": [ 3 | { 4 | "name": "Output (80%)", 5 | "wave": "h...lh...l" 6 | }, 7 | { 8 | "name": "Digital", 9 | "wave": "7...87...8", 10 | "data": ["ON", "OFF", "ON", "OFF"] 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /src/diagram_generator/wavedrom/spi_waveform.json: -------------------------------------------------------------------------------- 1 | { 2 | "signal": [ 3 | { 4 | "name": "SCLK", 5 | "wave": "1.lhlhlhlhlhlhlhlh." 6 | }, 7 | { 8 | "name": "CS", 9 | "wave": "1l................h" 10 | }, 11 | { 12 | "name": "MOSI", 13 | "wave": "x.3.3.3.3.3.3.3.3.x", 14 | "data": ["D7", "D6", "D5", "D4", "D3", "D2", "D1", "D0"] 15 | }, 16 | { 17 | "name": "MISO", 18 | "wave": "x.4.4.4.4.4.4.4.4.x", 19 | "data": ["Q7", "Q6", "Q5", "Q4", "Q3", "Q2", "Q1", "Q0"] 20 | } 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /src/images/I2C_controller-target.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/circuits-and-code/circuits-and-code-book/7cf76d1bb2a4bd97b9f964fc579af797cd4f7f2d/src/images/I2C_controller-target.png -------------------------------------------------------------------------------- /src/images/cover.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/circuits-and-code/circuits-and-code-book/7cf76d1bb2a4bd97b9f964fc579af797cd4f7f2d/src/images/cover.jpg -------------------------------------------------------------------------------- /src/images/diff_pair_noise_immunity.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/circuits-and-code/circuits-and-code-book/7cf76d1bb2a4bd97b9f964fc579af797cd4f7f2d/src/images/diff_pair_noise_immunity.png -------------------------------------------------------------------------------- /src/images/wikipedia_can_bus_topology.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/circuits-and-code/circuits-and-code-book/7cf76d1bb2a4bd97b9f964fc579af797cd4f7f2d/src/images/wikipedia_can_bus_topology.png -------------------------------------------------------------------------------- /src/images/wikipedia_diff_signal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/circuits-and-code/circuits-and-code-book/7cf76d1bb2a4bd97b9f964fc579af797cd4f7f2d/src/images/wikipedia_diff_signal.png -------------------------------------------------------------------------------- /src/interrupts.tex: -------------------------------------------------------------------------------- 1 | \documentclass[main.tex]{subfiles} 2 | \begin{document} 3 | 4 | \section{What is an interrupt service routine (ISR), and how does it differ from a regular function in implementation?} 5 | 6 | \spoilerline 7 | 8 | \subsection{Interrupts} 9 | An interrupt is a signal that \textit{interrupts} the currently-executing process on the CPU to handle a specific event. Interrupts are often used to handle time-sensitive tasks, such as input/output (I/O) operations, and are essential for real-time systems. This section summarizes general principles of interrupt service routines; more information can be found in embedded systems literature (e.g., \cite{white2024}, \cite{myersInterrupts}, and \cite{BetterEmbeddedSystemSoftware}). 10 | Some examples are as follows: 11 | \begin{itemize} 12 | \item \textbf{Timer Interrupts:} Used to keep track of time and schedule tasks. 13 | \item \textbf{I/O Interrupts:} Used to handle input/output operations. 14 | \item \textbf{Hardware Interrupts:} Used to signal hardware events, such as a button press. 15 | \end{itemize} 16 | \noindent An interrupt is a powerful construct as it allows the CPU to handle events without necessarily \textit{polling} for whether an event has occurred. This allows the CPU to perform other tasks while waiting for an event to occur. \newline 17 | \newline 18 | \noindent Elaborating on the button press interrupt example: the CPU can continue executing other tasks until the button is pressed, at which point the interrupt is triggered and the CPU can handle the button press. This is in contrast to polling, where the CPU would have to continuously check if the button is pressed, which is inefficient and wastes CPU cycles. 19 | 20 | \subsubsection{Theory of Operation Review} 21 | When an interrupt occurs, the CPU saves the current state of the program and pushes it on the stack, executes the \textit{interrupt service routine} (ISR), and then restores the program's state. \cite{white2024} 22 | \newline 23 | \newnoindentpara Broadly speaking, there are 2 types of interrupt handling mechanisms. Depending on the system architecture and interrupt source type, one or both may be used \cite{myersInterrupts}: 24 | \begin{itemize} 25 | \item \textbf{Non-Vectored/Polled Interrupts:} Interrupts are handled by a common ISR, which then determines the source of the interrupt. An example would be when 3 unique button interrupts are handled by a single ISR, which then determines which button was pressed. 26 | \item \textbf{Vectored Interrupts:} The interrupting device directly specifies the ISR to be executed; the address of the ISR to call is usually stored in a table of function pointers called the \textit{vector table}. An example would be when 3 button interrupts are handled by 3 separate ISRs, avoiding the need for the ISR to explicitly determine the button issuing the interrupt. 27 | \end{itemize} 28 | 29 | \newnoindentpara Several other concepts are important to understand when working with interrupts, but are not directly related to the question. These include: 30 | \begin{itemize} 31 | \item \textbf{Interrupt Priority:} Determines which interrupt is serviced first when multiple interrupts occur simultaneously. 32 | \item \textbf{Interrupt Nesting:} The ability to handle interrupts while another interrupt is being serviced. 33 | \item \textbf{Interrupt Masking:} Disabling interrupts to prevent them from being serviced. 34 | \end{itemize} 35 | 36 | \subsection{Interrupt Service Routines} 37 | An \textit{Interrupt Service Routine} (ISR) is a special type of function that is called when an interrupt occurs. Because they are called asynchronously and through hardware, ISRs differ from regular functions in several key ways. 38 | \begin{itemize} 39 | \item \textbf{Execution Time:} ISRs should be kept as short as possible, as they can block other interrupts from being serviced and stall the main program from running. This is especially important in real-time systems, where missing an interrupt can have serious consequences. In an RTOS, a key assumption made by deadline scheduling algorithms is that ISRs are extremely fast when compared to task execution time \cite{BetterEmbeddedSystemSoftware}. As a corollary, ISR's should avoid blocking operations. 40 | \item \textbf{Concurrency:} ISR's, by their very nature, are concurrent with the main program. This means that they can interrupt the main program at any time, and the main program must be written with this in mind. 41 | \begin{itemize} 42 | \item In particular, attention should be paid to shared variables and resources between the ISR and the main program, and may require the use of queues, semaphores, or other synchronization mechanisms. 43 | \item In addition, ISR's should avoid non-reentrant (\textit{reentrant functions are functions that can be called again, or re-entered, before a previous invocation completes}) functions, as they can be interrupted and cause unexpected behavior. Examples of non-reentrant functions include those that use global variables or static variables, as well as \texttt{malloc()} and \texttt{printf()}. 44 | \end{itemize} 45 | \item \textbf{No Return Value:} ISRs do not return a value, as they are not called by the program but by the hardware. 46 | \end{itemize} 47 | 48 | \end{document} -------------------------------------------------------------------------------- /src/keyword.tex: -------------------------------------------------------------------------------- 1 | \documentclass[main.tex]{subfiles} 2 | \begin{document} 3 | 4 | \section{Explain the following C keywords: \texttt{volatile}, \texttt{const}, and \texttt{static}.} 5 | For each keyword, explain its purpose, as well as where it would be beneficial to use it. 6 | 7 | \spoilerline 8 | 9 | \subsection{Volatile} 10 | The \textbf{volatile} keyword is used to tell the compiler that there are \textit{unexpected side-effects} associated with reads and writes of a variable, and as a result, should \textbf{not} optimize reads/writes to a variable declared as \texttt{volatile}. From \cite{wikipediaVolatile}, it tells the compiler to: 11 | \begin{itemize} 12 | \item \textbf{Not remove or add any reads/writes} to the variable, even if it appears unused in the program. Specifically, this ensures that the compiler does not optimize away accesses or cache the variable's value, forcing every read and write to occur exactly as written. This prevents incorrect behavior, especially when interacting with hardware registers or shared memory. 13 | \item \textbf{Not to reorder the reads/writes} to the variable with respect to other reads/writes in the program. 14 | \end{itemize} 15 | 16 | \noindent Note that using volatile where not necessary can lead to worse performance, as the compiler may not optimize the variable's access. 17 | 18 | \subsubsection{Example Volatile Use Case} 19 | Consider a short program below in Listing \ref{code:volatile-use-case} where a timer interrupt service routine (ISR) increments a variable \texttt{timer\_counter} every millisecond. 20 | 21 | \lstinputlisting[caption={Volatile Use Case}, label={code:volatile-use-case}]{code/keyword/volatile_ex.c} 22 | 23 | \noindent The main loop checks if the \texttt{timer\_counter}'s modulo (remainder) has reached a certain value and then performs an action. If the \texttt{timer\_counter} variable is not declared as \texttt{volatile}, the compiler may optimize the loop and cache the value of \texttt{timer\_counter} after reading it only once as the ISR\_Timer function does not appear to be called, causing the if statement to never be true. Effectively, the compiler assumes that you, the human, has written dead code (\textit{term for code that is unreachable}) and thinks it can optimize it away. By declaring \texttt{timer\_counter} as \texttt{volatile}, the compiler will always read the variable from memory, ensuring the loop works as expected. 24 | \newline 25 | \newline 26 | \textbf{Note:} A major use-case of volatile is to access memory-mapped I/O registers in embedded systems. These registers can change due to external events by the hardware (\textit{HW}) rather than code, and the compiler should not optimize the reads/writes to these registers. Memory-mapped I/O registers should be declared as such (ex: \texttt{volatile uint32\_t * const UART\_DR = (uint32\_t *)0x40000000;}) to avoid the aforementioned issues. 27 | 28 | \subsection{Const} 29 | The \textbf{const} keyword refers to a variable that is \textbf{read-only} \cite{beningoConst}. The compiler will throw an error if an attempt is made to modify a variable labeled as \texttt{const}. \textbf{Importantly, \texttt{const} does not mean that the value is constant, but rather that the variable cannot be modified by the program}. The distinction is important, as it's possible to have values that are declared as \texttt{const}, but are not constant. 30 | \newline 31 | \newline 32 | \noindent Const's primary use is to make code more readable and maintainable. By declaring a variable as \texttt{const}, the programmer can signal to others that the variable should not be modified and have the compiler enforce this behaviour. This can help prevent bugs and make the code easier to understand. As a general rule, it's good practice to declare variables as \texttt{const} whenever possible. 33 | 34 | \subsubsection{Example Const Use Case} 35 | Consider the code snippet in Listing \ref{code:const-use-case}. The variable \texttt{UART\_RECEIVE\_REGISTER\_READ\_ONLY} is declared as \texttt{const}, meaning its value cannot be modified through the program (a compiler error will be thrown). However, the value at the memory address \texttt{0x12345678} can still change due to external events, such as hardware (e.g., UART) writing to it,\footnote{It's also possible to bypass the protection of const, both intentionally and inadvertantly.} which is why \texttt{const} does not mean \textit{constant}. 36 | 37 | \begin{lstlisting}[caption={Example use case of Const}, label={code:const-use-case}] 38 | #include 39 | 40 | volatile const uint8_t* const UART_RECEIVE_REGISTER_READ_ONLY = (uint32_t*)0x12345678; 41 | 42 | int main() { 43 | uint8_t uart_receive_char = *UART_RECEIVE_REGISTER_READ_ONLY; // Valid C code! 44 | *UART_RECEIVE_REGISTER_READ_ONLY = 0xEF; // Compiler error: assignment of read-only location 45 | return 0; 46 | } 47 | \end{lstlisting} 48 | 49 | \subsection{Static} 50 | The \textbf{static} keyword has different meanings depending on the context in which it is used. 51 | \subsubsection{Static Functions} 52 | When used with functions, the \texttt{static} keyword limits the function's scope to the file in which it is defined (as all functions are implicitly declared as extern without the \texttt{static} qualifier \cite{arm_static}). This means that the function cannot be accessed by other files through linking. This is useful for helper functions that are only used within a single file and should not be exposed to other files, in effect creating a private function. Listing \ref{code:static-function} shows an example of a static function. 53 | 54 | 55 | \begin{lstlisting}[caption={Example of a Static Function}, label={code:static-function}] 56 | static void helper_function() { 57 | // Function implementation. This function can only be accessed within the file it is defined in. 58 | } 59 | \end{lstlisting} 60 | 61 | \subsubsection{Static Declarations} 62 | When used with global variables, the \texttt{static} keyword limits the variable's scope to the file in which it is defined \cite{arm_static}. This means that the variable cannot be accessed by other files through linking. This is useful for creating private global variables that are only accessible within a single file. Ex: declaring \texttt{static uint8\_t counter = 0U;} in a file means \texttt{counter} is accessible only within that file, and no where else. These variables are stored in the data segment, or Block Starting Symbol (BSS) segment if uninitialized, and retain their value throughout the program's execution.\footnote{For more information, see \href{https://cosmic-software.com/faq/faq17.php}{this FAQ entry on static variables}.} 63 | 64 | 65 | \subsubsection{Static Local Variables} 66 | When used with local variables within functions, the \texttt{static} keyword changes the variable's storage class to static. This means that the variable is stored in the data segment rather than the stack (where temporary data and variables are stored \cite{embedded_com_Static}), and its value is retained between function calls. This is useful when you want a variable to retain its value between function calls. Listing \ref{code:static-local-variable} shows an example of a static local variable. 67 | 68 | \lstinputlisting[caption={Static Local Variable}, label={code:static-local-variable}]{code/keyword/main.c} 69 | 70 | 71 | \end{document} -------------------------------------------------------------------------------- /src/main.tex: -------------------------------------------------------------------------------- 1 | \documentclass[letterpaper,11pt]{article} 2 | 3 | \usepackage[top=1in, bottom=1in, left=1in, right=1in]{geometry} 4 | \usepackage{subfiles} 5 | \usepackage[T1]{fontenc} 6 | \usepackage[sfdefault]{cabin} 7 | \usepackage[utf8]{inputenc} 8 | \usepackage{amsmath} 9 | \usepackage{graphicx} 10 | \usepackage{float} 11 | \usepackage[hidelinks]{hyperref} 12 | \usepackage{xcolor} 13 | \usepackage{titlesec} 14 | \usepackage{listings} 15 | \usepackage{courier} 16 | \usepackage[backend=biber,style=ieee]{biblatex} 17 | \usepackage{array} 18 | \usepackage{booktabs} 19 | \usepackage{multirow} 20 | \usepackage{nameref} 21 | \usepackage{circuitikz} % https://mirrors.ibiblio.org/CTAN/graphics/pgf/contrib/circuitikz/doc/circuitikzmanual.pdf 22 | \usepackage{wrapfig} 23 | \usepackage{titling} 24 | \usepackage{subcaption} 25 | \usepackage{caption} 26 | \usepackage{fancyhdr} 27 | \usepackage{eso-pic} 28 | 29 | % Configure Packages 30 | \renewcommand{\familydefault}{\sfdefault} 31 | \addbibresource{references.bib} 32 | \setlength{\belowcaptionskip}{-8pt} % Controls the space below the caption 33 | \pagestyle{fancy} % Use fancy style 34 | \hbadness=10000 % prevents LaTeX from reporting underfull boxes unless they are truly severe. 35 | \captionsetup{labelfont=it, textfont=it} % Makes all figure labels and captions italic 36 | \lstdefinestyle{modernC}{ % Listings settings for C language 37 | language=C, 38 | basicstyle=\ttfamily\small, % Use modern monospaced font 39 | keywordstyle=\color{blue}\bfseries, 40 | identifierstyle=\color{black}, 41 | commentstyle=\color{gray}\itshape, 42 | stringstyle=\color{teal}, 43 | numberstyle=\tiny\color{gray}, 44 | numbers=none, 45 | numbersep=8pt, 46 | backgroundcolor=\color{gray!10}, % Light gray background 47 | frame=single, 48 | tabsize=4, 49 | captionpos=b, 50 | breaklines=true, 51 | showstringspaces=false, 52 | escapeinside={\%*}{*)}, % Escaping for inline LaTeX 53 | morekeywords={uint8_t, uint16_t, uint32_t} % Custom keywords 54 | } 55 | \lstset{style=modernC} % Set the style for lists 56 | 57 | % Header 58 | \fancyhead[L]{} 59 | \fancyhead[C]{Circuits \& Code: Mastering Embedded Co-op Interviews} 60 | \fancyhead[R]{} 61 | 62 | % Footer 63 | \fancyfoot[C]{Page \thepage} 64 | \fancyfoot[R]{\bluehref{https://circuits-and-code.github.io}{circuits-and-code.github.io}} 65 | 66 | % Remove default header/footer rule lines if needed 67 | \renewcommand{\headrulewidth}{0.4pt} % Header line thickness (0pt removes it) 68 | \renewcommand{\footrulewidth}{0.4pt} % Footer line thickness (0pt removes it) 69 | 70 | % Our defined commands (used in subsheets) 71 | \newcommand{\bluehref}[2]{% 72 | \textcolor{blue}{\underline{\href{#1}{#2}}}% 73 | } 74 | \newcommand{\spoilerlineraw}{% 75 | \begin{center} 76 | ------------ \textbf{Answers Ahead} ------------ 77 | \end{center} 78 | } 79 | \newcommand{\spoilerline}{% 80 | \spoilerlineraw 81 | \begin{center} 82 | \textit{Remainder of page intentionally left blank. Solution begins on next page.} 83 | \end{center} 84 | \newpage 85 | } 86 | \newcommand{\newnoindentpara}{ 87 | \par 88 | \noindent 89 | } 90 | 91 | \begin{document} 92 | 93 | \subfile{title.tex} 94 | \newpage 95 | 96 | % Title Page 97 | \title{Circuits \& Code: Mastering Embedded Co-op Interviews} 98 | \author{\textcopyright \ Sahil Kale, Daniel Puratich} 99 | % \date{} % By leaving this blank it defaults to the build date, as the book is being continually improved, leaving this blank seems optimal 100 | \setlength{\droptitle}{0.3\textheight} % Moves the text down from the top 101 | \maketitle % displays title, author, and build date field 102 | \thispagestyle{empty} % Removes the page number on the title page 103 | \newpage 104 | 105 | \tableofcontents 106 | \newpage 107 | 108 | \subfile{preamble.tex} 109 | \newpage 110 | 111 | % Questions 112 | \subfile{bitstream_parity.tex} 113 | \newpage 114 | \subfile{voltage_divider.tex} 115 | \newpage 116 | \subfile{interrupts.tex} 117 | \newpage 118 | \subfile{led.tex} 119 | \newpage 120 | \subfile{keyword.tex} % place after interrupts.tex 121 | \newpage 122 | \subfile{signalling.tex} 123 | \newpage 124 | \subfile{pid.tex} 125 | \newpage 126 | \subfile{compare-i2c-spi.tex} % place after signalling.tex 127 | \newpage 128 | \subfile{spi-bitbang.tex} % place after compare-i2c-spi.tex 129 | \newpage 130 | \subfile{oscilloscope.tex} % place after voltage_divider.tex, signalling.tex 131 | \newpage 132 | \subfile{what-is-can.tex} 133 | \newpage 134 | \subfile{passives.tex} % place after voltage_divider.tex, oscilloscope.tex, signalling.tex, compare-i2c-spi.tex 135 | \newpage 136 | \subfile{packet-parsing.tex} 137 | \newpage 138 | \subfile{current_sense.tex} 139 | \newpage 140 | \subfile{adc_init.tex} 141 | \newpage 142 | % \subfile{switching.tex} % place after current_sense.tex, led.tex, passives.tex % Add back in when it's completed 143 | % \newpage 144 | \subfile{64-bit-timer.tex} 145 | \newpage 146 | \subfile{stack-growth.tex} 147 | \newpage 148 | \subfile{opamp.tex} 149 | \newpage 150 | \subfile{mutex_vs_semaphore.tex} 151 | \newpage 152 | \subfile{buck_vs_ldo.tex} % place after passives.tex, opamp.tex 153 | \newpage 154 | 155 | % Appendix 156 | \subfile{appendix_packet_parsing_tests.tex} 157 | \newpage 158 | \subfile{appendix_switched_divider.tex} 159 | \newpage 160 | \subfile{appendix_more_passives.tex} 161 | \newpage 162 | \subfile{appendix_more _opamps.tex} 163 | \newpage 164 | 165 | % Bibliography 166 | \printbibliography 167 | 168 | \end{document} 169 | -------------------------------------------------------------------------------- /src/mutex_vs_semaphore.tex: -------------------------------------------------------------------------------- 1 | \documentclass[main.tex]{subfiles} 2 | \begin{document} 3 | 4 | \section{What are the differences between a mutex and a semaphore, and in what use cases are each typically employed?} 5 | 6 | \spoilerline 7 | 8 | \subsection{Introduction} 9 | \noindent A mutex and a semaphore are both synchronization primitives that can be used to signal and/or synchronize access to shared resources in a multi-threaded environment. This section summarizes general principles of interrupt service routines, as commonly described in embedded systems literature - DigiKey has excellent articles on the implementation of real-time operating systems (RTOS) and synchronization primitives \cite{digikey_rtos}. 10 | 11 | \subsection{Semaphore} 12 | A \textit{semaphore} (also known as a \textit{counting semaphore}) is a signalling mechanism that allows a thread to signal one or more waiting threads that a particular event has occurred. It can be thought of as a shared non-negative integer counter. 13 | \newline 14 | \newline 15 | Using a semaphore involves two main operations: \begin{itemize} 16 | \item \texttt{signal\_semaphore()} - increments the semaphore counter 17 | \item \texttt{wait\_for\_semaphore\_signal(timeout)} - decrements the semaphore count, and if the count is less than zero, blocks the calling thread for the duration of the timeout until the count is greater than zero. 18 | \end{itemize} 19 | 20 | \noindent A semaphore is often used in the following scenarios: 21 | \begin{itemize} 22 | \item Controlling access to a pool of resources 23 | \begin{itemize} 24 | \item Ex: A server thread with a limited number of connections - the semaphore count would represent the number of available connections, with the semaphore being decremented when a connection is acquired and incremented when a connection is released. A client thread would wait for the semaphore to be incremented before attempting to acquire a connection. 25 | \end{itemize} 26 | \item Signalling to thread(s) that a particular event has occurred. In the context of an RTOS, where there is only one core available, a semaphore can be used to signal an event to a higher priority task from a lower priority task, and have the higher priority task run instantly. This is because the higher priority task will preempt the lower priority task once it is no longer 'blocked' by the semaphore as it will be available for the higher priority task to acquire. 27 | \begin {itemize} 28 | \item Ex: An interrupt service routine (ISR) can signal to a worker thread that an event has occurred. \textit{Note: Event flags are also commonly used for this purpose, if provided by the OS.} 29 | \end{itemize} 30 | \end{itemize} 31 | A short example of using a semaphore is shown in Listing \ref{code:semaphore-example}. 32 | \begin{lstlisting}[caption={Example of Semaphore Usage}, label={code:semaphore-example}] 33 | semaphore_wait(conn_semaphore); // Wait for an available connection 34 | use_connection(); 35 | semaphore_signal(conn_semaphore); // Release the connection 36 | \end{lstlisting} 37 | 38 | \subsection{Mutex} 39 | A \textit{mutex} (short for \textit{mutual exclusion}) is a signalling mechanism that is used to prevent multiple threads from accessing a shared resource simultaneously. It can be thought of as a lock that is either locked or unlocked. 40 | \newline 41 | \newline 42 | \noindent Using a mutex involves two main operations: \begin{itemize} 43 | \item \texttt{lock\_mutex(timeout)} - attempts to lock the mutex. If the mutex is already locked, the function blocks the calling thread for the duration of the timeout until the mutex is unlocked. 44 | \item \texttt{unlock\_mutex()} - unlocks the mutex, allowing other threads to lock it. 45 | \end{itemize} 46 | 47 | \noindent Consider a UART driver that is being accessed by multiple threads. If the UART driver is not thread-safe, it is possible that two threads could attempt to write to the UART at the same time, causing garbled output. In this case, a mutex could be used to ensure that only one thread can access the UART at a time. Pseudocode for this scenario is shown in Listing \ref{code:mutex-example}. 48 | \begin{lstlisting}[caption={Example of Mutex Usage}, label={code:mutex-example}] 49 | mutex_lock(uart_mutex, DELAY_INFINITE); 50 | uart_write("Hello"); 51 | mutex_unlock(uart_mutex); 52 | \end{lstlisting} 53 | 54 | \subsection{Putting it together} 55 | The key differences between a mutex and a semaphore are that 56 | \begin{itemize} 57 | \item A mutex only has 2 states, locked and unlocked, while a semaphore can count. 58 | \item A mutex has a concept of ownership, while a semaphore does not. This means that the thread that locks a mutex must be the one to unlock it, while any thread can signal or wait on a semaphore. 59 | \item A mutex is typically used to protect access to a shared resource, while a semaphore is typically used to signal an event or control access to a pool of resources. 60 | \end{itemize} 61 | 62 | \subsection{Priority Inversion and Inheritance} 63 | A common follow-up question is to ask about the potential consequences of using a mutex in a real-time system. One of the most common issues is \textit{priority inversion}, which occurs when a high-priority task is blocked by a lower-priority task that holds a mutex. An example of this situation is shown in Figure \ref{fig:priority_inversion}. In this scenario, the high-priority task is blocked by the mutex held by the low-priority task. Since the medium priority task is of higher priority than the low-priority task, it will run before the low-priority task, causing the high-priority task to be blocked for an extended period of time. This can lead to missed deadlines and degraded system performance. 64 | 65 | \begin{figure}[H] 66 | \centering 67 | \includegraphics[scale=0.55]{generated_images/priority_inversion.png} 68 | \caption{Priority inversion diagram.} 69 | \label{fig:priority_inversion} 70 | \end{figure} 71 | 72 | \subsubsection{Priority Inheritance} 73 | A solution to fix this is called \textit{priority inheritance} - the OS will temporarily boost the priority of the low-priority task to the priority of the high-priority task while the low-priority task holds the mutex \cite{digikey_priority_inversion}. This ensures that the high-priority task can run as soon as the low-priority task releases the mutex. An example of this situation is shown in Figure \ref{fig:priority_inheritance}. 74 | 75 | \begin{figure}[H] 76 | \centering 77 | \includegraphics[scale=0.55]{generated_images/priority_inheritance.png} 78 | \caption{Priority inheritance diagram.} 79 | \label{fig:priority_inheritance} 80 | \end{figure} 81 | 82 | \subsection{Follow-ups} 83 | \begin{itemize} 84 | \item Describe other RTOS features that can be used for inter-task communication. 85 | \item Explain how deadlock can occur in an RTOS and how it can be prevented. 86 | \end{itemize} 87 | 88 | \end{document} 89 | -------------------------------------------------------------------------------- /src/opamp.tex: -------------------------------------------------------------------------------- 1 | \documentclass[main.tex]{subfiles} 2 | 3 | \begin{document} 4 | 5 | \section{Solve the transfer function for the following circuit.} \label{section:opamp} 6 | 7 | % \noindent Note: in a real interview scenario, it's reasonable to only expect just one of these circuits. 8 | % Numerous circuits were initially proposed for this question for the sake of building intuition and the showing first principles approach, however, it is optimal to show a single circuit to reduce scope creeping this question 9 | 10 | \begin{figure}[H] 11 | \begin{center} 12 | \begin{circuitikz}[american, transform shape] 13 | \draw (0,0) node[left]{$V_{in}$} to[short] ++ (1,0) 14 | node[op amp, noinv input up, anchor=+](op-amp){} 15 | (op-amp.-) -- ++(0,-1) coordinate(FB) 16 | to[R=$R_i$] ++(0,-2) node[ground]{} 17 | (FB) to[R=$R_f$] (FB -| op-amp.out) -- (op-amp.out) 18 | to [short] ++(1,0) node[right]{$V_{out}$}; 19 | \end{circuitikz} 20 | \caption{Given Circuit} 21 | \label{fig:non_inverting_amp} 22 | \end{center} 23 | \end{figure} 24 | % Chose this circuit because it can be simplified into unity gain amplifier and isn't as complicated as the difference amplifier 25 | 26 | \spoilerline 27 | 28 | \subsection{Operational Amplifiers} 29 | Ideal operational amplifiers (\textit{op-amps}) are active devices characterized by the following equations: 30 | 31 | \begin{equation} 32 | \begin{aligned} 33 | V_{out} &= A \cdot (V_{+} - V_{-}) \quad \text{where} \quad A \to \infty, \\ 34 | I_{+} &= I_{-} = 0. 35 | \end{aligned} 36 | \label{eq:op-amp-governing-equations} 37 | \end{equation} 38 | 39 | \begin{figure}[H] 40 | \begin{center} 41 | \begin{circuitikz}[american, transform shape] 42 | \draw (0,0) node[op amp, yscale=-1] (opamp) {}; 43 | \draw (opamp.out) -- ++ (0.5, 0) node[right]{$V_{out}$}; 44 | \draw (opamp.out) to[short, i=$I_{out}$] ++ (0.5, 0); 45 | \draw (opamp.+) -- ++ (-0.5, 0) node[left]{$V_{+}$} to[short, i=$I_{+}$] (opamp.+); 46 | \draw (opamp.-) -- ++ (-0.5, 0) node[left]{$V_{-}$} to[short, i=$I_{-}$] (opamp.-); 47 | \end{circuitikz} 48 | \caption{Labelled Op-amp} 49 | \label{fig:labelled_op-amp} 50 | \end{center} 51 | \end{figure} 52 | 53 | \subsubsection{Virtual Short} 54 | \noindent When an op-amp is connected in a negative feedback configuration, the inputs ($V_{+}$ and $V_{-}$) are considered "virtually shorted", meaning $V_{+} = V_{-}$. Negative feedback in an op-amp circuit means there is a current path from it's output, $V_{out}$, to it's inverting terminal, $V_{-}$. 55 | 56 | \subsection{Solving} 57 | \noindent When analyzing circuits, begin by defining equations for simple elements and build from there. In this circuit, Ohms' law can be applied to the $R_f$ and $R_i$ resistors, which results in the following relationships: $V_{out} - V_{-} = I \cdot R_f$ and $V_{-} = I \cdot R_i$. Note in this case, $I_{-} = 0$ as the current ($I$) through resistor $R_f$ is the same as the current through $R_i$. \newline 58 | 59 | \newnoindentpara Next, because $R_f$ connects a current path from $V_{out}$ to $V_{-}$, the virtual short assumption holds for the op-amp - therefore, $V_{+} = V_{-}$. From the circuit, it's seen that $V_{in} = V_{+}$, so $V_{in} = V_{+} = V_{-}$. This conclusion can be substituted into the above-derived resistor equations to get $V_{out} - V_{in} = I \cdot R_f$ and $V_{in} = I \cdot R_i$. \newline 60 | 61 | \newnoindentpara These equations can be substituted into each other to cancel out $I$ and algebraically rearranged to determine Equation \ref{eq:non-inverting-transfer-function}. The solution for $\frac{V_{out}}{V_{in}}$ is known as the transfer function of the circuit. The concept of a transfer function is used to analyze numerous circuits. 62 | 63 | \begin{equation} 64 | \mathbf{\frac{V_{out}}{V_{in}} = 1 + \frac{R_f}{R_i}} 65 | \label{eq:non-inverting-transfer-function} 66 | \end{equation} 67 | 68 | \subsubsection{Intuition} 69 | To understand this circuit better, consider how this circuit operates. 70 | \begin{itemize} 71 | \item Because negative resistors don't exist, $R_f > 0$ and $R_i > 0$, $\frac{V_{out}}{V_{in}} > 1$ always. As the gain of the circuit is always positive, this circuit is referred to as a \textit{Non-inverting Amplifier}. 72 | \item When $R_f = R_i = R$ the transfer function simplifies into $\frac{V_{out}}{V_{in}} = 2$. 73 | \item When $R_f >> R_i$ the gain becomes very large, $\frac{V_{out}}{V_{in}} \approx \infty$. 74 | \item When $R_i >> R_f$ the gain approaches unity, $\frac{V_{out}}{V_{in}} \approx 1$. 75 | \end{itemize} 76 | 77 | \subsection{Unity Gain Amplifier} 78 | A special case of this circuit occurs when $R_f$ is replaced with a short circuit, $R_f = 0$, and $R_i$ is replaced with an open circuit, $R_i = \infty$ as drawn in Figure \ref{fig:unity-amp}. 79 | 80 | \begin{figure}[H] 81 | \begin{center} 82 | \begin{circuitikz}[american, transform shape] 83 | \draw (0,0) node[left]{$V_{in}$} to[short] ++ (1,0) 84 | node[op amp, noinv input up, anchor=+](op-amp){} 85 | (op-amp.-) -- ++(0,-1) coordinate(FB) 86 | (FB) to[short] (FB -| op-amp.out) -- (op-amp.out) 87 | to [short] ++(1,0) node[right]{$V_{out}$}; 88 | \end{circuitikz} 89 | \caption{Unity Gain Amplifier Circuit} 90 | \label{fig:unity-amp} 91 | \end{center} 92 | \end{figure} 93 | 94 | \noindent In this case the transfer function becomes unity, $\frac{V_{out}}{V_{in}} = 1$ which can be simplified into $V_{out} = V_{in}$. \newline 95 | 96 | \newnoindentpara This circuit acts as a current buffer. Because $I_{V_{+}} = 0$ and $V_{in} = V_{+}$ we see that $I_{in} = 0$ so the circuit doesn't load the input at all. Instead, any current required by the load is provided by the op-amp! 97 | 98 | % \subsection{Non-idealities} 99 | % While most interview questions focus on ideal op-amps, real op-amps suffer from numerous non-idealities. The most important of which should be understood to aid in circuit design questions and are described below. 100 | 101 | % \subsubsection{Saturation} 102 | % Saturation occurs when 103 | % For a rail to rail op-amp, $V_{out}$ is bounded by the rails, $V_{ee}$ and $V_{cc}$, such that $V_{ee} < V_{out} < V_{cc}$. % more details required here to explain 104 | 105 | % \subsubsection{Input Offset Voltage} 106 | % The input offset voltage is the minimum voltage difference that an amplifier can detect between $V_{+}$ and $V_{-}$ input pins. 107 | 108 | % \subsubsection{Input Bias Current} 109 | % Input bias current is current, $I_{in}$, into $V_{+}$ and $V_{-}$. This current is usually very small, but has an affect on the circuit! 110 | 111 | \subsection{Follow-ups} 112 | \begin{itemize} 113 | \item What is an inverting amplifier? 114 | \item What is a difference amplifier? 115 | \item Describe the non-idealities of op-amps and how to compensate for them? % Saturation, Input offset voltage, input bias current 116 | \item Why are non-inverting amplifiers preferred over inverting amplifiers? % Input Impedance 117 | \item Solve the circuits in extra practice question \ref{extra_practice:more_opamps}. 118 | % \item What is an Instrumentation Amplifier? When would you use it? % Non-inverting amplifier's buffering a difference amplifier 119 | \end{itemize} 120 | 121 | \end{document} 122 | -------------------------------------------------------------------------------- /src/packet-parsing.tex: -------------------------------------------------------------------------------- 1 | \documentclass[main.tex]{subfiles} 2 | \begin{document} 3 | 4 | \section{Implement a C bytestream parsing function for a weather data sensor.} \label{section:packet_parsing} 5 | Use the header in Listing \ref{code:bytestream-header}. Note that within the \texttt{weather\_data\_t} structure, the temperature is stored as a float with units of degrees Celsius, while the pressure is stored as a float with units of \textbf{kilopascals}. The function will only be called on a complete packet (i.e. the entire packet is received before the function is called). 6 | 7 | \lstinputlisting[caption={Bytestream Parsing Header}, label={code:bytestream-header}]{code/packet_parsing/packet_parsing_header.h} 8 | 9 | \subsection{Sample Datasheet} 10 | The weather sensor outputs a UART packet with the following format. A graphical version of the packet format is shown in Figure \ref{fig:packet-data}. 11 | \begin{table}[H] 12 | \centering 13 | \begin{tabular}{|c|l|} 14 | \hline 15 | \textbf{Byte} & \textbf{Description} \\ \hline 16 | 0 & Start of Frame (0x55) \\ \hline 17 | 1 & Temperature data (Celsius) - Integer part + 32 ($T_{integer} = Byte_{1} - 32$) \\ \hline 18 | 2 & Temperature data (Celsius) - Fractional part (0-100) \\ \hline 19 | 3 & Pressure data (Pa - Integer Part) - MSB \\ \hline 20 | 4 & Pressure data (Pa - Integer Part) \\ \hline 21 | 5 & Pressure data (Pa - Integer Part) \\ \hline 22 | 6 & Pressure data (Pa - Integer Part) - LSB \\ \hline 23 | 7 & Checksum - Lowest byte of the sum of bytes 0-6 \\ \hline 24 | \end{tabular} 25 | \caption{Byte Descriptions for Weather Sensor UART Packet} 26 | \label{table:byte_description} 27 | \end{table} 28 | 29 | 30 | \begin{figure}[H] 31 | \centering 32 | \includegraphics[width=1.0\textwidth]{generated_images/svg_generated/packet_parsing.png} 33 | \caption{Weather Sensor Packet Format} 34 | \label{fig:packet-data} 35 | \end{figure} 36 | 37 | \spoilerlineraw 38 | 39 | \subsection{Packet Parsing Algorithm} 40 | A packet parsing algorithm is a staple of embedded systems programming. Generally, an implementation of such an algorithm involves receiving a buffer (packet) of bytes (also referred to as a \textit{bytestream}), followed by iterating through the buffer and extracting relevant data. If present in the packet, a checksum is used to verify the integrity of the data. 41 | 42 | \subsubsection{Checksum} 43 | A checksum is a simple error-detection method\footnote{In real-world implementations, a more robust method, like a cyclic redundancy check (CRC) is usually used.} that involves summing the bytes of a packet and comparing the result to a predefined value. If the checksum is incorrect, the packet is considered corrupt. In this case, the checksum is the sum of bytes 0-6. A simple algorithm for checksum calculation is shown in Listing \ref{code:checksum_ex}. 44 | 45 | \lstinputlisting[caption={Checksum Calculation Example}, label={code:checksum_ex}]{code/packet_parsing/checksum_ex.c} 46 | 47 | \noindent For this example, the checksum is calculated by summing the bytes from 1 to 6. However, the checksum that is transmitted is the lowest byte of the sum. This is done to save bandwidth and reduce the number of bytes transmitted. It is equivalent to saying that \texttt{checksum\_transmitted = checksum\_calculated \& 0xFF}. 48 | 49 | \subsection{Parsing Function Implementation} 50 | When implementing a parsing function, it's important to consider the failure cases that require attention in our implementation (and usually make up most of what interviewers are expecting). In the scope of this question, the edge cases include: 51 | \begin{itemize} 52 | \item The packet length is incorrect (the length is fixed and the packet is assumed to be completely received, therefore, any buffer length that is not equal to the expected packet length is considered an error). 53 | \item The buffer or data packet arguments are \texttt{NULL}. 54 | \item The start of frame byte (0x55) is not found at the beginning of the packet. 55 | \item The checksum is incorrect. 56 | \end{itemize} 57 | 58 | \noindent A sample implementation of the parsing function is shown in Listing \ref{code:packet_parsing}. Note that type-punning is explicitly not used in this implementation to avoid dealing with endianness, alignment, and platform portability issues, although it is a common practice in embedded systems programming. 59 | 60 | \lstinputlisting[caption={Packet Parsing Function}, label={code:packet_parsing}]{code/packet_parsing/parsing_implementation.c} 61 | 62 | \subsubsection{Testing} 63 | In some interviews, an interviewer may ask you to write a test function to verify that the parsing function works correctly. The list of edge cases mentioned above can be used to write test cases - this is further elaborated upon in \nameref{extra_practice:packet_parsing_tests}. 64 | 65 | \subsection{Follow-ups} 66 | \begin{itemize} 67 | \item How would you modify the parsing function to handle a packet with a different checksum algorithm? 68 | \item How would you modify the parsing function to handle asynchronous data transmission (i.e. fragmented packets)? 69 | \end{itemize} 70 | 71 | \end{document} -------------------------------------------------------------------------------- /src/pid.tex: -------------------------------------------------------------------------------- 1 | \documentclass[main.tex]{subfiles} 2 | \begin{document} 3 | 4 | \section{Implement a PID controller in C and discuss its typical applications.} \label{section:pid} 5 | 6 | \spoilerline 7 | 8 | \subsection{Introduction} 9 | \noindent A PID (\textit{Proportional-Integral-Derivative}) controller is common type of feedback controller. It's ubiquity comes from its relatively easy implementation and effectiveness in a wide range of control systems. Its typical applications include \textbf{motor speed/position control, temperature control, lighting regulation}, and many more. The theory behind PID controllers is considered to be out of scope for this guide, but Tim Wescott's article titled \bluehref{http://www.wescottdesign.com/articles/pid/pidWithoutAPhd.pdf}{PID Without a PhD} provides a great introduction to PID controllers without delving into linear control theory. 10 | 11 | \subsection{PID Controller Implementation} 12 | The form of a PID controller is given by Equation \eqref{eq:pid_controller} \cite{AbramovitchPID}. $K_p$ is the proportional gain, $K_i$ is the integral gain, and $K_d$ is the derivative gain, with $u(t)$ being the desired controller output. The error term $e(t)$ is the difference between the desired setpoint (i.e. reference) and the system output (i.e. process variable). The integral term is the sum of all past errors, and the derivative term is the rate of change of the error.\footnote{If the terms integral and derivative are unfamiliar, an excellent resource is \bluehref{https://www.khanacademy.org/math/calculus-1}{Khan Academy's calculus courses here}. These concepts are typically covered in an introductory calculus course.} The equation can appear daunting, but the implementation is quite straightforward. 13 | \begin{equation} 14 | u(t) = K_p e(t) + K_i \int_{0}^{t} e(\tau) d\tau + K_d \frac{de(t)}{dt} 15 | \label{eq:pid_controller} 16 | \end{equation} 17 | 18 | \noindent First, a C structure definition is created to hold the PID gains, as well as temporary variables for calculating the integral and derivative terms. This structure is shown in Listing \ref{code:pid}. 19 | \lstinputlisting[caption={PID Control Structure}, label={code:pid}]{code/pid/pid_typedef.h} 20 | 21 | \noindent Breaking apart the problem into smaller functions, we can implement the terms of the equation as follows in C. Note that \texttt{dt} is the timestep (period) between control loop iterations. 22 | \begin{itemize} 23 | 24 | \item The proportional term is simply the product of the proportional gain and the error. It is responsible for increasing system responsiveness, but can cause overshoot. \newline 25 | \texttt{const float p\_term = pid->K\_p * error;}. 26 | 27 | \item The integral term accumulates the error over time, summing up all past errors. Applying a control signal proportional to the integral-error helps reduce steady-state error. To approximate this integral, we use the commonly-chosen Backward Euler method \cite{AbramovitchPID}, which updates the integral (sum) by adding the product of the current error and the timestep. Note that the computation of the \texttt{i\_term} comes after the addition of the integral in this approximation. See Figure \ref{fig:pid-backward-euler-integral} for a visual representation of the Backward Euler method. 28 | \newline 29 | \texttt{pid->integral += error * dt;}. \newline 30 | \texttt{const float i\_term = pid->K\_i * pid->integral;}. 31 | \begin{figure}[H] 32 | \centering 33 | \includegraphics[width=0.8\textwidth]{generated_images/pid_discretization_integral.png} 34 | \caption{Backward Euler Discretization of the Integral Term} 35 | \label{fig:pid-backward-euler-integral} 36 | \end{figure} 37 | 38 | \item The derivative term is the rate of change of the error, and can help reduce overshoot. To approximate this derivative, we use the Backward Euler method, which calculates the derivative (slope) as the difference between the current error and the previous error, divided by the timestep ($\text{slope} = \dfrac{\text{rise}}{\text{run}}$). See Figure \ref{fig:pid-backward-euler-derivative} for a visual representation of the Backward Euler method. 39 | \newline 40 | \texttt{const float d\_term = pid->K\_d * ((error - pid->prev\_error) / dt);}. 41 | \begin{figure}[H] 42 | \centering 43 | \includegraphics[width=0.8\textwidth]{generated_images/pid_discretization_derivative.png} 44 | \caption{Backward Euler Discretization of the Derivative Term} 45 | \label{fig:pid-backward-euler-derivative} 46 | \end{figure} 47 | 48 | \end{itemize} 49 | 50 | \noindent Putting it all together, the PID controller step function is shown in Listing~\ref{code:pid_controller}. Note the added check for a \texttt{NULL} pointer and positive timestep as either can cause the function to not run correctly. 51 | 52 | \lstinputlisting[caption={PID Controller Step}, label={code:pid_controller}]{code/pid/pid_controller.c} 53 | 54 | \subsection{Follow-ups} 55 | \begin{itemize} 56 | \item \textbf{Anti-windup}: What is integral windup, and how can it be prevented in a PID controller? 57 | \item \textbf{Tuning}: What effect does adjusting the gains $K_p$, $K_i$ and $K_d$ have on the system's response? 58 | \item \textbf{Filtering}: What are possible implications of using a poorly filtered signal with a PID controller? 59 | \end{itemize} 60 | 61 | \end{document} 62 | -------------------------------------------------------------------------------- /src/preamble.tex: -------------------------------------------------------------------------------- 1 | \documentclass[main.tex]{subfiles} 2 | \begin{document} 3 | 4 | \section{Introduction} 5 | 6 | This guide was created by two Waterloo Engineering students, \bluehref{https://www.linkedin.com/in/daniel-puratich}{Daniel Puratich} and \bluehref{https://www.linkedin.com/in/sahil-kale}{Sahil Kale}. Wanting to support our peers in their co-op journeys, we noticed that we were often answering the same questions when asked about co-op interview prep. Noticing that many firmware and hardware co-op interviews focus on fundamental concepts, we decided to compile a comprehensive list of commonly asked questions and answers to help students better prepare for their interviews. The book also has an accompanying website, which includes additional content and resources. You can find it at \bluehref{https://circuits-and-code.github.io}{circuits-and-code.github.io}. 7 | 8 | \subsection{Target Audience} 9 | Our guide is designed for engineering undergraduates seeking technical internships in embedded software, firmware, and electrical engineering across the US and Canada. The questions and answers aim to provide a foundational understanding, making them ideal for those just starting in the field. The content is also relevant for more experienced students looking to refine their skills and prepare for technical interviews. 10 | 11 | \subsection{How to Use This Guide} 12 | This guide is structured to help you understand and answer common technical questions asked in firmware and hardware co-op interviews. Each section starts by introducing a question and relevant informational content, followed by a detailed answer. We recommend reading through the content and attempting to answer the questions yourself before reviewing the provided answers. Answers begin after this line: 13 | 14 | \spoilerlineraw 15 | 16 | \noindent \textbf{It's important to note that many questions in this guide have multiple correct answers.} We've provided detailed explanations for each question to help you understand the concepts and reasoning behind our answers, and pointed out context-specific considerations where applicable. The 'best' answer will depend on the context of the question and the interviewer's expectations - remember that engineering is about problem-solving and tradeoff decision-making, and there are often multiple ways to approach a problem. 17 | 18 | \subsection{Extra Practice} 19 | The guide also features \textit{extra practice}, which are extensions on top of the questions already discussed in this book. However, these extra practice questions do not have full explanations and are provided for intuition building as they are often common follow-up questions. 20 | 21 | \subsection {Breadth of Topics} 22 | Embedded systems cover a broad range of topics that intersect hardware and software. While most questions in this guide are broadly applicable to both firmware and hardware roles, some are tailored to specific industries. For example, topics like mutexes versus semaphores are more likely to appear in firmware interviews, whereas buck converter vs low dropout regulator is more likely to appear in EE interviews. \textbf{We recommend focusing on the questions most relevant to your target role while gaining a general understanding of both fields.} 23 | 24 | \subsubsection{Code} 25 | Code snippets are included to illustrate various embedded software concepts. The code is written in C and can be copied and compiled at your discretion, though some snippets may require the inclusion of additional header files and minor restructuring. Note that while snippets can be compiled, they are not intended to be standalone programs or run unless a \texttt{main} function is defined. 26 | 27 | \noindent Note that code snippets are available at the \bluehref{https://github.com/circuits-and-code/circuits-and-code-book/tree/main}{Circuits and Code Book GitHub} (where a star is very much appreciated!): 28 | 29 | \subsubsection{LaTeX} 30 | This guide was written using Latex and the source code can be found at \bluehref{https://github.com/circuits-and-code/circuits-and-code-book/tree/main}{Circuits and Code Book GitHub}. This shows the code we used to generate plots, draw circuits, and create diagrams as well as comments containning more pilosophical thoughts for those interested in peering behind the curtain. 31 | 32 | \subsection{Acronyms} 33 | This guide features numerous acronyms and phrases that are commonly accepted in industry. We will provide definitions for these acronyms the first time we use them, but will assume you understand them in subsequent questions as understanding these terms will be integral to understanding common interview questions. 34 | 35 | \subsection{About the Authors} 36 | \bluehref{https://www.linkedin.com/in/sahil-kale}{Sahil} interned at Tesla, Skydio, and BETA Technologies, focusing on real-time embedded software for safety-critical control systems. 37 | \newline 38 | \newline 39 | \bluehref{https://www.linkedin.com/in/daniel-puratich}{Daniel} interned at Tesla, Anduril Industries, and Pure Watercraft, focusing on power electronics and board design. \newline 40 | 41 | \newnoindentpara We met at the Waterloo Aerial Robotics Group (WARG), a student team that designs and builds autonomous drones. Uniquely, we have experience in hiring and interviewing multiple co-op students, giving us insight into the interview process from both sides, as well as an understanding of what responses are expected from candidates. We value mentorship, enjoy sharing our knowledge, and take pride in helping others succeed in their co-op journeys. 42 | % I don't want to say led past tense for myself so maybe we add that in after i step down lol. 43 | 44 | \subsubsection{Other Work} 45 | We've published other (free!) guides to help engineering students land firmware and hardware co-op roles. Check them out: 46 | \begin{itemize} 47 | \item \bluehref{https://docs.google.com/document/d/1Qh0Jp70ce2ParzsWV4TizEAvGyTrDO5WfkacxrHjTFs}{The Sahil and Daniel Co-op Resume Guide} - focuses on resume writing and tailoring for hardware and firmware roles. 48 | \item \bluehref{https://docs.google.com/document/d/12qFdJfc2ve2jIFIFHqtIzx0qkN6I5H43kfLo0OQ1X_A}{The Sahil and Daniel Co-op Process Guide} - offers our tips and tricks to landing a co-op role in hardware and firmware. 49 | \end{itemize} 50 | 51 | \subsection{Acknowledgements} 52 | We'd like to thank the several individuals for taking the time to review and provide feedback on this guide. Their names can be found in the \bluehref{https://circuits-and-code.github.io/acknowledgements/}{acknowledgements page} on our book's website. Please feel free to reach out to us with feedback as we are looking to improve the guide over time! 53 | 54 | \subsection{Disclaimer} 55 | This book is designed to be an educational resource, drawing from the authors' experiences and research. While we've done our best to ensure accuracy, readers are encouraged to use their own judgment and explore additional resources as needed. The authors and publisher are not responsible for any errors or omissions. Please note, the content is for informational purposes only and is not intended as professional advice. \newline 56 | 57 | \newnoindentpara \textcopyright \ Sahil Kale, Daniel Puratich | 2025 58 | 59 | \end{document} 60 | -------------------------------------------------------------------------------- /src/references.bib: -------------------------------------------------------------------------------- 1 | @book{white2024, 2 | author = {Elecia White}, 3 | title = {Making Embedded Systems}, 4 | edition = {2nd}, 5 | year = {2024}, 6 | publisher = {O'Reilly Media}, 7 | } 8 | 9 | @misc{wikipediaVolatile, 10 | author = {Wikipedia}, 11 | title = {Volatile (computer programming)}, 12 | url = {https://en.wikipedia.org/wiki/Volatile_(computer_programming)}, 13 | accessdate = {2025-02-19} 14 | } 15 | 16 | @misc{myersInterrupts, 17 | author = {Chris J. Myers}, 18 | title = {Lecture 9: Interrupts in the 6812}, 19 | year = {n.d.}, 20 | institution = {Rose Hulman}, 21 | note = {Lecture notes for ECE/CS 5780/6780: Embedded System Design}, 22 | url = {https://www.rose-hulman.edu/class/ee/hoover/ece331/old%20stuff/my%20csm12c32%20downloads/lec9-2x3.pdf}, 23 | accessdate = {2024-12-23} 24 | } 25 | 26 | @book{BetterEmbeddedSystemSoftware, 27 | author = {Philip Koopman}, 28 | title = {Better Embedded System Software, 1st Edition, Revised 2021}, 29 | year = {2021}, 30 | } 31 | 32 | @article{beningoConst, 33 | author = {Jacob Beningo}, 34 | title = {Embedded Basics: Peculiarities of the Keyword \texttt{const}}, 35 | year = {2015}, 36 | url = {https://www.beningo.com/embedded-basics-peculiarities-of-the-keyword-const/}, 37 | note = {Accessed: 2024-12-23} 38 | } 39 | 40 | @misc{embedded_com_Static, 41 | author = {Embedded Staff}, 42 | title = {The C Keyword: \texttt{static}}, 43 | year = {2014}, 44 | url = {https://www.embedded.com/the-c-keyword-static/}, 45 | note = {Accessed: 2024-12-23} 46 | } 47 | 48 | @misc{arm_static, 49 | author = {Jacob Beningo}, 50 | title = {Using the \texttt{static} Keyword in C}, 51 | year = {2014}, 52 | url = {https://community.arm.com/arm-community-blogs/b/embedded-blog/posts/using-the-static-keyword-in-c}, 53 | note = {Accessed: 2024-12-23} 54 | } 55 | 56 | @misc{digikey_priority_inversion, 57 | author = {Shawn Hymel}, 58 | title = {Introduction to RTOS: Solution to Part 11 - Priority Inversion}, 59 | year = {2021}, 60 | publisher = {Digi-Key Electronics}, 61 | url = {https://www.digikey.com/en/maker/projects/introduction-to-rtos-solution-to-part-11-priority-inversion/abf4b8f7cd4a4c70bece35678d178321}, 62 | note = {Accessed: 2024-12-23} 63 | } 64 | 65 | @misc{digikey_rtos, 66 | author = {Shawn Hymel}, 67 | title = {What is a Real-Time Operating System (RTOS)?}, 68 | year = {2021}, 69 | publisher = {Digi-Key Electronics}, 70 | url = {https://www.digikey.com/en/maker/projects/what-is-a-realtime-operating-system-rtos/28d8087f53844decafa5000d89608016}, 71 | note = {Accessed: 2024-12-23} 72 | } 73 | 74 | @article{AbramovitchPID, 75 | author={Abramovitch, Daniel Y.}, 76 | booktitle={2015 IEEE Conference on Control Applications (CCA)}, 77 | title={A unified framework for analog and digital PID controllers}, 78 | year={2015}, 79 | volume={}, 80 | number={}, 81 | pages={1492-1497}, 82 | keywords={Mathematical model;PD control;Time-domain analysis;Gain;MATLAB;Filtering;Frequency control}, 83 | doi={10.1109/CCA.2015.7320822} 84 | } 85 | 86 | @misc{looi_bitbanging, 87 | author = {Looi Kian Seong}, 88 | title = {Introduction to Bit-Banging}, 89 | year = {2020}, 90 | url = {https://medium.com/@kslooi/introduction-to-bit-banging-46e114db3466}, 91 | note = {Accessed: 2024-12-23} 92 | } 93 | 94 | @misc{seander_bithacks, 95 | author = {Sean Eron Anderson}, 96 | title = {Bit Twiddling Hacks}, 97 | year = {n.d.}, 98 | url = {https://graphics.stanford.edu/~seander/bithacks.html}, 99 | note = {Accessed: 2024-12-23} 100 | } 101 | 102 | @online{cadence_canbus_history, 103 | title = {CAN Bus History at a Glance}, 104 | author = {{Cadence}}, 105 | year = {n.d.}, 106 | url = {https://resources.pcb.cadence.com/blog/2022-can-bus-history-at-a-glance}, 107 | note = {Accessed: 2024-12-25} 108 | } 109 | 110 | @online{wikipedia_differential_signalling, 111 | author = {{Gutten på Hemsen}}, 112 | title = {Differential signalling}, 113 | year = {n.d.}, 114 | url = {https://en.wikipedia.org/wiki/Differential_signalling#/media/File:Differential_signal_fed_into_a_differential_amplifier.svg}, 115 | note = {Accessed: 2024-12-25, CC-BY-SA 4.0} 116 | } 117 | 118 | @MISC {stackoverflow_diff_pair_noise, 119 | TITLE = {Noise can be Differential or Common!}, 120 | AUTHOR = {Andy aka (https://electronics.stackexchange.com/users/20218/andy-aka)}, 121 | HOWPUBLISHED = {Electrical Engineering Stack Exchange}, 122 | NOTE = {URL:https://electronics.stackexchange.com/q/231301 (version: 2016-04-29), CC-BY-SA}, 123 | EPRINT = {https://electronics.stackexchange.com/q/231301}, 124 | URL = {https://electronics.stackexchange.com/q/231301} 125 | } 126 | 127 | @online{wikipedia_can_bus_image, 128 | author = {{Stefan-Xp}}, 129 | title = {CAN Bus — Elektrische Zweidrahtleitung}, 130 | year = {n.d.}, 131 | url = {https://en.wikipedia.org/wiki/CAN_bus#/media/File:CAN-Bus_Elektrische_Zweidrahtleitung.svg}, 132 | note = {Accessed: 2024-12-25, CC-BY-SA 3.0} 133 | } 134 | 135 | @online{ti_can_signal_levels, 136 | author = {{John Griffith - Texas Instruments}}, 137 | title = {What Do CAN Bus Signals Look like?}, 138 | year = {2023}, 139 | url = {https://www.ti.com/document-viewer/lit/html/SSZTCN3#:~:text=As%20you%20can%20see%2C%20in,potential%20(approximately%201.5V).}, 140 | note = {Accessed: 2024-12-25} 141 | } 142 | 143 | @online{sparkfun_i2c_history, 144 | author = {{SparkFun Electronics}}, 145 | title = {I2C}, 146 | year = {n.d.}, 147 | url = {https://learn.sparkfun.com/tutorials/i2c/a-brief-history-of-i2c}, 148 | note = {Accessed: 2024-12-26} 149 | } 150 | 151 | @online{wikipedia_i2c_bus_image, 152 | author = {{Tim Mathias}}, 153 | title = {Example I2C Schematic}, 154 | year = {2021}, 155 | url = {https://en.wikipedia.org/wiki/I%C2%B2C#/media/File:I2C_controller-target.svg}, 156 | note = {Accessed: 2024-12-26, CC-BY-SA 4.0} 157 | } 158 | 159 | @online{SPI_history, 160 | author = {{Total Phase}}, 161 | title = {SPI Background}, 162 | year = {n.d.}, 163 | url = {https://www.totalphase.com/support/articles/200349236-spi-background/?srsltid=AfmBOooNHnQYWKPxz4YWdKyz9Bp4tEq3lw87j9EO6zr8YzMyRQkNYJz1}, 164 | note = {Accessed: 2024-12-26} 165 | } 166 | 167 | @online{wikipedia_SPI_bus, 168 | author = {{User:Cburnett}}, 169 | title = {SPI Three Slaves}, 170 | year = {2006}, 171 | url = {https://commons.wikimedia.org/wiki/File:SPI_three_slaves.svg}, 172 | note = {Accessed: 2024-12-26, CC-BY-SA 3.0} 173 | } -------------------------------------------------------------------------------- /src/spi-bitbang.tex: -------------------------------------------------------------------------------- 1 | \documentclass[main.tex]{subfiles} 2 | \begin{document} 3 | 4 | \section{Implementing a bit-bang'ed SPI master.} 5 | 6 | \subsection{Given Code} 7 | Assume the header shown in Listing \ref{code:bit-bang-header} is available for use in the bit-banging implementation of SPI. The transceive function should use CPOL = 1, CPHA = 1. 8 | 9 | \lstinputlisting[caption={Bit Bang HAL Header}, label={code:bit-bang-header}]{code/bitbang_spi/bitbang_spi_header.h} 10 | 11 | \spoilerline 12 | 13 | \subsection{Bit-Banging Basics} 14 | In embedded systems, communication protocols like SPI are typically handled by dedicated hardware peripherals. These peripherals manage the precise timing and fast data transmission required to communicate over physical wires. However, it is also possible to implement these protocols purely in software, a technique commonly known as \textit{bit-banging}. Bit-banging can be useful in the following scenarios: 15 | \begin{itemize} 16 | \item When a hardware peripheral (ex: SPI/I2C/UART) is unavailable on the microcontroller, usually because it's not supported, or the EE (\textit{Electrical Engineer}) gave the pins a 'creative reassignment' (accidentally routed incorrect pins during schematic capture). 17 | \item When a custom or non-standard protocol is required, which cannot be supported by existing hardware peripherals. 18 | \end{itemize} 19 | 20 | \noindent Bit-banging is usually implemented by manually controlling the individual pins of a microcontroller in software to emulate a hardware peripheral \cite{looi_bitbanging}. While this approach is slower and uses more CPU resources, it provides flexibility for situations where hardware support is limited. Listing \ref{code:bit-bang-square-ex} shows an example of a square wave generator implemented using bit-banging. Note the use of \texttt{delayMicroseconds()} to control the timing of the square wave, which blocks the CPU until the desired time has passed, contributing to the inefficiency of bit-banging. 21 | 22 | \lstinputlisting[caption={Bit-Banged Square Wave Implementation}, label={code:bit-bang-square-ex}]{code/bitbang_spi/bitbang_ex.c} 23 | 24 | \subsection{SPI Review} 25 | Consider the SPI timing diagram in Figure \ref{fig:spi_timing_2}. The SPI protocol consists of 4 signals: MOSI (Master Out Slave In), MISO (Master In Slave Out), SCLK (Serial Clock), and CS (Chip Select). The master device controls the clock signal and selects the device using the CS signal; data is transmitted on MOSI and received on MISO simultaneously on predefined edges of the clock signal. 26 | 27 | \begin{figure}[H] 28 | \centering 29 | \includegraphics[scale=0.4]{generated_images/svg_generated/spi_waveform.png} 30 | \caption{1-byte SPI Timing Diagram} 31 | \label{fig:spi_timing_2} 32 | \end{figure} 33 | 34 | \noindent Note that the SPI protocol can be configured in different clock sampling modes, which define the clock polarity (CPOL) and phase (CPHA). Figure \ref{fig:spi_timing} shows CPOL = 1, CPHA = 1. A SPI slave device's datasheet will usually specify the required CPOL and CPHA settings for proper communication real-world implementation. 35 | \begin {itemize} 36 | \item \textbf{CPOL (Clock Polarity)}: Determines the idle state of the clock signal. CPOL = 0 means the clock is low when idle, while CPOL = 1 means the clock is high when idle. 37 | \item \textbf{CPHA (Clock Phase)}: Determines when data is sampled and changed. CPHA = 0 means data is sampled on the leading edge (first transition) of the clock, while CPHA = 1 means data is sampled on the trailing edge (second transition) of the clock. 38 | \end{itemize} 39 | 40 | \subsection{Bit-banging SPI Implementation} 41 | Going through the timing diagram in Figure \ref{fig:spi_timing} piece by piece, we can implement the SPI protocol in software. The key parts are as follows: 42 | \begin{itemize} 43 | \item Pull the CS line low to select the slave device. Wait for a brief period (referred to as \textit{setup time}). 44 | \item Generate a square wave on the SCLK line, ensuring the correct CPOL and CPHA settings. 45 | \item Loop through each bit of the tx byte starting from the most significant bit (MSB), and transmit the logical value of the bit on the MOSI line on every SCLK falling edge. 46 | \item In parallel, read the MISO line on the rising edge of SCLK line to receive the slave's response, and write it to the corresponding bit in the received byte buffer. 47 | \item Repeat for the specified number of bytes. 48 | \item Terminate the SPI transaction by pulling the CS and SCLK line high to deselect the slave device. 49 | \end{itemize} 50 | 51 | \noindent Listing \ref{code:bit-bang-spi} shows a basic implementation of a bit-banged SPI master transceive. 52 | 53 | \lstinputlisting[caption={Bit Bang SPI Implementation}, label={code:bit-bang-spi}]{code/bitbang_spi/bitbang_spi.c} 54 | 55 | \end{document} 56 | -------------------------------------------------------------------------------- /src/stack-growth.tex: -------------------------------------------------------------------------------- 1 | \documentclass[main.tex]{subfiles} 2 | 3 | \begin{document} 4 | 5 | \section{Write a C function to determine the direction of stack growth on a system.} 6 | 7 | \spoilerline 8 | 9 | \subsection{Understanding the Stack} 10 | The stack is a region of memory that is used to store local variables and function call information. The stack uses a \textit{stack} data structure, meaning that data is organized with by last-in, first-out (LIFO) organization. This means that the last item placed on the stack is the first item to be removed. The stack is used to store local variables, function arguments, and the return address of the function.\footnote{It's worth noting that the direction of stack growth is usually a strictly academic question - there is usually little use in only knowing the direction of stack growth.} This is opposed to \textit{heap memory}, which is allocated \textit{dynamically} and is used to store data that persists beyond the scope of a function call and is managed by the user using \texttt{malloc} and \texttt{free}. 11 | 12 | \subsubsection{Stack Frame Example} 13 | When a function is called, a new \textit{stack frame} is pushed onto the call stack. This stack frame typically includes the return address of the calling function, local variables, and sometimes arguments for the function. When the function returns, its stack frame is popped off the stack, and execution resumes from the stored return address. The term stack trace refers to examining the stack's contents to understand the sequence of function calls that led to the current state, along with associated information like return addresses and (in some cases) function arguments. 14 | \newline 15 | \newnoindentpara Consider the following C snippet in this example, on a system where the stack grows downwards (from higher addresses to lower addresses). 16 | \lstinputlisting[caption={Stack Example}, label={code:stack-ex}]{code/stack/stack_ex/main.c} 17 | 18 | \noindent Generally speaking, the stack might look something like what is found in Table \ref{table:stack_overview} if we paused inside the \texttt{multiply} function. Note the stack grows downwards in this example system, hence the decrease in address values. Also note that a new stack frame is created for each function call. 19 | 20 | \begin{table}[H] 21 | \centering 22 | \begin{tabular}{|c|c|c|c|} 23 | \hline 24 | \textbf{Address} & \textbf{Function} & \textbf{Called From} & \textbf{Arguments} \\ \hline 25 | 0x5000 & \texttt{multiply} & \texttt{add\_and\_multiply} & \texttt{c = 10, d = 20} \\ \hline 26 | 0x6000 & \texttt{add\_and\_multiply} & \texttt{main} & \texttt{a = 10, b = 20} \\ \hline 27 | 0x7000 & \texttt{main} & \texttt{\_start} & \texttt{(N/A)} \\ \hline 28 | \end{tabular} 29 | \caption{High-level overview of the stack during program execution in \texttt{multiply}.} 30 | \label{table:stack_overview} 31 | \end{table} 32 | 33 | \subsection{Determining Stack Growth Direction} 34 | Since we know that a new stack frame is created for each function call, we can use this property to determine the direction of stack growth. The steps to determine the direction of stack growth are as follows: 35 | \begin{enumerate} 36 | \item Create a main function that calls a dummy function. Pass in a pointer to a stack-allocated variable to the dummy function. 37 | \item In the dummy function, compare the address of the stack-allocated variable to the address of a local variable in the dummy function. 38 | \item If the address of the stack-allocated variable is less than the address of the local variable, the stack grows downwards. If the address of the stack-allocated variable is greater than the address of the local variable, the stack grows upwards. 39 | \end{enumerate} 40 | \noindent The code snippet in Listing \ref{code:stack-growth} demonstrates this concept. 41 | 42 | \lstinputlisting[caption={Determining Stack Growth Direction}, label={code:stack-growth}]{code/stack/main.c} 43 | 44 | \end{document} 45 | -------------------------------------------------------------------------------- /src/switching.tex: -------------------------------------------------------------------------------- 1 | \documentclass[main.tex]{subfiles} 2 | 3 | \begin{document} 4 | 5 | \section{Design a circuit to allow a microcontroller to control a solenoid.} \label{section:switching} 6 | 7 | The solenoid operates at $\approx 12\text{V}$ DC requiring $\approx 1\text{A}$ of current. The microcontroller operates at a $3.3\text{V}$ logic level and must be able to enable and disable the solenoid. Power is supplied to the circuit from a bench top power supply. 8 | 9 | \spoilerline 10 | 11 | \subsection{Solenoids \& Flyback} 12 | A solenoid is an electromagnet based on a coil of wire usually wrapped around a magnetic core to concrentrate the magnetic field. These devices see numerous applications in electro-mechanical systems such as valves and circuit breakers. As the solenoid is made of a long wire, it has resistance. \newline 13 | 14 | \newnoindentpara Due to the coil of wire, a solenoid is highly inductive and requires extra considerations to control in an embedded system. Consider when a solenoid is enabled the question states $I = 1\text{A}$ so when disabling the solenoid the goal is to switch to a $I = 0 \text{A}$ state. If this state switch is performed quickly then the inductive load can see a very negative $\dfrac{d\text{I}}{d\text{t}}$ as $\dfrac{0 - 1}{\approx 0} = - \infty$. For an inductor $V_L = L \cdot \dfrac{d\text{I}}{d\text{t}}$ so this can lead to a very negative $V_L$ which can cause damage to the circuitry responsible for enabling and disabling the load. \newline 15 | 16 | \newnoindentpara This is commonly compensated for by using a Schottky diode accross the solenoid so that during normal operation the diode does nothing, but if the voltage accross the coil, $V_L$, goes negative then the diode conducts allowing the solenoid to dissipate it's energy without damaging the switching circuitry. As this diode protects the circuit from the flyback voltage spike induced by the inductive load it is referred to as a Flyback diode. 17 | 18 | \begin{figure}[H] 19 | \begin{center} 20 | \begin{circuitikz}[american] 21 | \draw (0, 3) to[inductor] (0, 1.5) to[resistor] (0, 0); 22 | \draw (0, 3) -- (0, 3.25); 23 | \draw (0, 0) -- (0, -0.25); 24 | \draw (-2, 1.5) node[emptysdiodeshape, rotate=90] (d1) {}; 25 | \draw[thick] (-1, 3.5) rectangle (1, -0.5) node[]{}; 26 | \node[] at (0, 3.75) {Solenoid}; 27 | \draw (0, 3.1) -- (-2, 3.1) -- (-2, 1.75); 28 | \draw (0, -0.1) -- (-2, -0.1) -- (-2, 1.25); 29 | \end{circuitikz} 30 | \caption{Basic Solenoid Model with Flyback Diode} 31 | \label{fig:solenoid_with_flyback} 32 | \end{center} 33 | \end{figure} 34 | 35 | \subsection{Switching Loads} 36 | Enabling and disabling a load can be done in with either switching the positive high side while connecting the low side constantly or by switching the lower voltage potential side while holding the higher voltage potential side connected. \newline 37 | 38 | \newnoindentpara For any common microcontroller, GPIO pins by themselves will not be capable of switching a load with these voltage and current requirements so the use of a pass element transistor is used. The selected transistor needs to be capable of handling the load's voltage and current requirements while being capable of being controlled from the microcontroller. 39 | 40 | \subsection{Transistors} 41 | As introduced in Question \ref{section:led}, a BJT or FET could be selected for this application.\footnote{Of course other options exist, however, for a simple application these are the only options worthy of consideration.} 42 | 43 | As BJTs consume current at the base and have a constant base to emitter voltage drop, holding a BJT on requires constant power consumption. MOSFETs on the other hand require a gate to source voltage, but once the gate capacitance is filled they do not require any current to hold enabled. Additionally consider that the conduction losses of MOSFETs is much lower than BJTs. Consequently for driving loads, MOSFETs are prefferred! \newline 44 | 45 | WIP 46 | 47 | % TODO WRITE : PCh FETS have more RDSon (and therefore conduction losses) than Nch FETs so 48 | 49 | \subsection{High Side vs Low Side} 50 | 51 | WIP 52 | 53 | % Ground is more common around the system, espeacilly cars with grounded chassis 54 | 55 | % think of high voltage, switching neutral would make unscrewing a lightbulb unsafe 56 | 57 | % harnesses can share a ground wire (or use a chassis for ground) 58 | 59 | % common mode offset 60 | 61 | % TODO DRAW : high side switched solenoid 62 | 63 | % The primary advantage of low side switching is that it is easier to switch a transistor on the low side rather than the high side. 64 | 65 | % the existence of other ground referenced digital signals incentivizes high side switching for the loads 66 | 67 | % referenced to ground 68 | 69 | % TODO DRAW : low side switched solenoid 70 | 71 | % \subsection{Follow-ups} 72 | % \begin{itemize} 73 | % \item 74 | % \end{itemize} 75 | 76 | \end{document} 77 | -------------------------------------------------------------------------------- /src/title.tex: -------------------------------------------------------------------------------- 1 | \documentclass[main.tex]{subfiles} 2 | \begin{document} 3 | \thispagestyle{empty} 4 | 5 | \begin{center} 6 | \includegraphics[width=0.85\textwidth]{images/cover.jpg} 7 | \end{center} 8 | 9 | 10 | \end{document} --------------------------------------------------------------------------------