diff --git a/docs/chips-api/getting-started.md b/docs/chips-api/getting-started.md index 1dacc2b..055dc20 100644 --- a/docs/chips-api/getting-started.md +++ b/docs/chips-api/getting-started.md @@ -22,7 +22,7 @@ Custom Chips are usually written in C, but you can use any language that compile ## Tutorials - [Video Tutorial](https://youtu.be/yzdCS3A4DvU) - Three chip examples in 15 Minutes -- [Step-by-step blog tutorial](https://link.wokwi.com/chips-api-tutorial) - Create a 7-segment driver chip from scratch +- [Step-by-step blog tutorial](tutorial-7seg.md) - Create a 7-segment driver chip from scratch - [I2C chip tutorial](https://youtu.be/BS_uTqz3zik) - Coding a PCF8575 I/O expander from scratch ## Getting started diff --git a/docs/chips-api/tutorial-7seg.md b/docs/chips-api/tutorial-7seg.md new file mode 100644 index 0000000..c4373ed --- /dev/null +++ b/docs/chips-api/tutorial-7seg.md @@ -0,0 +1,262 @@ +--- +title: 'Tutorial: 7-segment display' +sidebar_label: 'Tutorial: 7-segement display' +--- + +# Tutorial: 7-segment display + +## Introduction +The Custom Chips API allows you to create new simulation models and behaviors that extend the functionality of Wokwi. +You can create new sensors, displays, memories, testing instruments and even simulate your own hardware. + +Custom chips are usually written in C, and have an accompanying JSON file that describes the pinout, as well as any +input values for the chip (e.g. the current temperature for a temperature sensor chip). Other languages are also +available - more on that later. + +In this tutorial we'll learn how to get started with the Chips API by implementing a simple 7-segment controller chip. +The chip will get a character (0-9 or A-F) via UART interface, and will display it on a +[7-segment display](https://docs.wokwi.com/parts/wokwi-7segment#using-the-7-segment-display). + +Let's get started! + +## The pinout +Before we dive into the code, let's define the pinout for the chip: + +| Name | Type | Function | +| :- | :- | :- | +| VCC | Power | Supply voltage | +| GND | Power | Ground | +| RX | Input | UART | +| SEG_A | Output | 7-segment | +| SEG_B | Output | 7-segment | +| SEG_C | Output | 7-segment | +| SEG_D | Output | 7-segment | +| SEG_E | Output | 7-segment | +| SEG_F | Output | 7-segment | +| SEG_G | Output | 7-segment | + +Our chip will have a total of 10 pins: two power supply pins, one UART input pin (RX), and 7 output pins to drive the +7-segment display. For simplicity, we'll assume that the 7-segment display is a common anode display, which is the +default on Wokwi. + +## The chip JSON file +Now we're ready to start writing code! We'll start from an empty ESP32-C3 project: +[wokwi.com/projects/new/esp32-c3](https://wokwi.com/projects/new/esp32-c3). + +The first thing we need to do is to create a custom chip. The easiest way to go about this is to press the blue "+" +button and serach for "Custom Chip". After selecting this option, type "sevseg-controller" for the chip name. Select +the "C" language option. + +Wokwi will add a green breakout board for your custom chip, and create two new files in your project: +- `sevseg-controller.chip.json` - defines the pinout +- `sevseg-controller.chip.c` - defines the logic for the chip + +We'll start by editing the `sevseg-controller.chip.json` as follows: +1. Change the `name` of the chip to "7 Segment Controller" +2. Change the `author` of the chip to your name +3. Change the `pins` array of the chip to include all the pin names: + ```json + ["VCC", "GND", "RX", "SEG_A", "SEG_B", "SEG_C", "SEG_D", "SEG_E", "SEG_F", "SEG_G"] + ``` + +You'll see the green breakout board updating as you make changes to the JSON file. + +## Implementing the chip's logic +Next, go to `sevseg-controller.chip.c`. This file implements the chip logic. The two important parts are: +1. The `chip_state_t` struct - use it to store all the state information of your chip, together with all the objects + that you create for your chips: IO pins, timers, etc. +2. The `chip_init` function - Wokwi will call this function for every instance (copy) of your chip. The function should + allocate memory for a new `chip_state_t` struct, initialize all the IO pins, the chip's state, and create any + relevant objects (we'll see an example in a minute). + +The default implementation does not include any state or initialization - it only prints a message saying "Hello from +custom chip!". You will see this message when you start the simulation - it'll appear in a new "Chips Console" tab +below the diagram. + +We'll modify `chip_init` to perform the following actions: +1. Initialize all the `SEG_x` pins as outputs +2. Listen for UART data on the `RX` pin, and call a function that will update the `SEG_x` according to the character we + received. + +### Initializing the 7-segment outputs +Start by adding a `segment_pins` array to the `chip_state_t` struct. This array will store a reference to the `SEG_x` +output pins, so we'll need to allocate 7 items: +```C +typedef struct { + pin_t segment_pins[7]; +} chip_state_t; +``` + +Next, add the following code to `chip_init` (after the line that defines `chip`): +```C +chip->segment_pins[0] = pin_init("SEG_A", OUTPUT_HIGH); +chip->segment_pins[0] = pin_init("SEG_B", OUTPUT_HIGH); +chip->segment_pins[0] = pin_init("SEG_C", OUTPUT_HIGH); +chip->segment_pins[0] = pin_init("SEG_D", OUTPUT_HIGH); +chip->segment_pins[0] = pin_init("SEG_E", OUTPUT_HIGH); +chip->segment_pins[0] = pin_init("SEG_F", OUTPUT_HIGH); +chip->segment_pins[0] = pin_init("SEG_G", OUTPUT_HIGH); +``` + +The code initializes each of the segment pins as an output, and sets the initial value to digital high. The 7-segment +display has a common annode, so setting a segment pin high will turn that segment off. You can learn more about the +`pin_init` function in the [GPIO API reference](../chips-api/gpio.md). + +### Listening to UART data +Add the following code to `chip_init`, right after the code that initializes the segment pins: +```C +const uart_config_t uart_config = { + .tx = NO_PIN, + .rx = pin_init("RX", INPUT), + .buad_rate = 115200, + .rx_data = on_uart_rx_data, + .user_data = chip, +}; +uart_init(&uart_config); +``` + +The code configures the `RX` pin as an input (in the third line), and sets up a `uart_config_t` structure. This +structure configures the baud rate, as well as a function that will get called whenever there is new data, +`on_uart_rx_data`. + +Also note how we set `.user_data` to `chip` - this is important, as this value will be passed as a parameter to the +`on_uart_rx_data` function, providing it access to our chip's state. + +In our case, we are only interested in receiving data, so we set `.tx` to the special `NO_PIN` value. + +:::tip + +To learn more about using UART in Wokwi, check out the [UART API reference](../chips-api/uart.md). + +::: + +### From UART to 7-segment +For the final part of the show, we'll implement the `on_uart_rx_data` callback. Paste the following code above the +definition of `chip_init`: +```C +const uint8_t font[] = { + ['0'] = 0b11000000, + ['1'] = 0b11111001, + ['2'] = 0b10100100, + ['3'] = 0b10110000, + ['4'] = 0b10011001, + ['5'] = 0b10010010, + ['6'] = 0b10000010, + ['7'] = 0b11111000, + ['8'] = 0b10000000, + ['9'] = 0b10010000, + ['A'] = 0b10001000, + ['B'] = 0b10000011, + ['C'] = 0b11000110, + ['D'] = 0b10100001, + ['E'] = 0b10000110, + ['F'] = 0b10001110, +}; + +static void on_uart_rx_data(void *user_data, uint8_t byte) { + chip_state_t *chip = user_data; + uint8_t font_char = font[byte]; + if (font_char) { + for (int bit = 0; bit < 7; bit++) { + uint8_t bit_value = font_char & (1 << bit); + pin_write(chip->segment_pins[bit], bit_value ? HIGH : LOW); + } + } +} +``` + +This part simply defines the "font" - it maps between a character that we receive from UART and the corresponding +segments that need to be turned on. Our 7-segment display has a common anode, so 0 will ligh a segment, and 1 will turn +it off. + +The `on_uart_rx_data` is where the actual magic happens. We use the `font` array to lookup the `byte` we received over +UART. If we find a match (when `font_char` is not 0), we iterate over the bits of the `font_char`, and update each +segment to its corresponding bit in `font_char`. + +That's it - we created a simple 7-segment controller chip for Wokwi! + +## Testing the chip +You can test the chip by adding a 7-segment display to the diagram, and writing it to the chip. Don't forget to write +the common pin of the 7-segment display to the 3.3V or 5V pin of the ESP32-C3 board! + +Next, wire the `RX` pin of the chip to the `TX` pin of the ESP32-C3 board. You can also wire the GND/VCC pins of the +chip for good measures, even though the chip will be functional even without these pins. + +Finally, pase the following code into `sketch.ino`: +```C +void setup() { + Serial.begin(115200); +} + +int i = 0; +void loop() { + Serial.println(i, HEX); + i++; + if (i > 0xf) { + i = 0; + } + delay(500); +} +``` + +It's a simple program that outputs all the hexadecimal values between 0 and F to the ESP32-C3's serial port - all the +characters that are included in our custom chip's font. If you prefer plain C code, you can change the first line in +`loop` to use `printf()` instead. +```C +printf("%X\n", i); +``` + +When you start the simulation, you should see the 7-segment display counting from 0 to F repeatedly. Hooray! + +Doesn't work? No worries, here's the link to the final result, so you can compare it with yours: +[wokwi.com/projects/371252876830114817](https://wokwi.com/projects/371252876830114817). + +## Under the hood +How do custom chips work? What happens with the C code you write? Behind the scenes, Wokwi takes this code and compiles +it into a Web Assembly module using LLVM (if you are curious, here's the +[Docker container](https://github.com/wokwi/wokwi-builders/blob/main/clang-wasm/Dockerfile) that does all the magic). + +When you run the simulation, Wokwi creates an instance of the Web Assembly module, and calls `chip_init` once for +every instance of the chip in your diagram. + +Using Web Assembly means you can write your code in a variety of languages. Currently, only C is officially supported, +there are some examples of how to write custom chips with Rust, AssemblyScript and even Zig. + +There's even a hack where we use Custom Chips to simulate Verilog: we use Yosys CXXRTL to convert your Verilog code +into C++, and then +[use emscripten to compile](https://github.com/wokwi/wokwi-builders/blob/main/verilog-cxxrtl/project/compile.sh) +the result along with [some glue code](https://github.com/wokwi/wokwi-builders/blob/main/verilog-cxxrtl/project/main.cpp) +into Web Assembly. Scary cool? + +## Next steps +If you want to dive deeper into the Custom Chips API, here are some ideas how to build on the chip we created in this +tutorial: + +- **Fix the bug!**
+ Unfortunately, our code has a bug - some values will cause it to display garbage on the 7-segment display (try sending + it a 'b'). Some bound checking can help! + +- **Add support for common cathode 7-segment displays**
+ You can use an additional input pin to select between common anode/cathode, or use the [Attributes API](../chips-api/attributes.md) + to allow the user to define the type of display by editing the chip attributes in `diagram.json`. + +- **Add another communication protocol**
+ You can turn our 7-segment display into an [I2C](../chips-api/i2c.md) or an [SPI](../chips-api/spi.md) device. + +- **Support multiple digits**
+ Control a two or four digital 7-segment display! Use the [Time API](../chips-api/time.md) to create a timer that will quickly + alternate between the digits. + +- **Add analog input**
+ Use the [Analog API](../chips-api/analog.md) to read and display an analog input value. This makes the 7-segment controller chips + useful even without a microcontroller - you can connect it directly to a potentiometer or an analog sensor, and + display the reading directly. + +- **Share your chip on GitHub**
+ By sharing your chip's code on GitHub, you can make it easy for other users to include it in their project. You can + use the [inverter-chip repo](https://github.com/wokwi/inverter-chip) as a starting point - it has a + [GitHub action](https://github.com/wokwi/inverter-chip/blob/main/.github/workflows/build.yaml) that automatically + compiles the chips and creates a release whenever you push a tag. + + Here's an [example for a Wokwi project](https://wokwi.com/projects/350946636543820370) that uses this chip. Note the + "dependencies" section in `diagram.json` - it tells Wokwi where to look for the chip implementation on GitHub. \ No newline at end of file diff --git a/sidebars.js b/sidebars.js index b245aef..777aef6 100644 --- a/sidebars.js +++ b/sidebars.js @@ -76,6 +76,7 @@ module.exports = { ], 'Chips API': [ 'chips-api/getting-started', + 'chips-api/tutorial-7seg', 'chips-api/chip-json', 'chips-api/gpio', 'chips-api/analog',