|
| 1 | +--- |
| 2 | +title: 'Tutorial: 7-segment display' |
| 3 | +sidebar_label: 'Tutorial: 7-segement display' |
| 4 | +--- |
| 5 | + |
| 6 | +# Tutorial: 7-segment display |
| 7 | + |
| 8 | +## Introduction |
| 9 | +The Custom Chips API allows you to create new simulation models and behaviors that extend the functionality of Wokwi. |
| 10 | +You can create new sensors, displays, memories, testing instruments and even simulate your own hardware. |
| 11 | + |
| 12 | +Custom chips are usually written in C, and have an accompanying JSON file that describes the pinout, as well as any |
| 13 | +input values for the chip (e.g. the current temperature for a temperature sensor chip). Other languages are also |
| 14 | +available - more on that later. |
| 15 | + |
| 16 | +In this tutorial we'll learn how to get started with the Chips API by implementing a simple 7-segment controller chip. |
| 17 | +The chip will get a character (0-9 or A-F) via UART interface, and will display it on a |
| 18 | +[7-segment display](https://docs.wokwi.com/parts/wokwi-7segment#using-the-7-segment-display). |
| 19 | + |
| 20 | +Let's get started! |
| 21 | + |
| 22 | +## The pinout |
| 23 | +Before we dive into the code, let's define the pinout for the chip: |
| 24 | + |
| 25 | +| Name | Type | Function | |
| 26 | +| :- | :- | :- | |
| 27 | +| VCC | Power | Supply voltage | |
| 28 | +| GND | Power | Ground | |
| 29 | +| RX | Input | UART | |
| 30 | +| SEG_A | Output | 7-segment | |
| 31 | +| SEG_B | Output | 7-segment | |
| 32 | +| SEG_C | Output | 7-segment | |
| 33 | +| SEG_D | Output | 7-segment | |
| 34 | +| SEG_E | Output | 7-segment | |
| 35 | +| SEG_F | Output | 7-segment | |
| 36 | +| SEG_G | Output | 7-segment | |
| 37 | + |
| 38 | +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 |
| 39 | +7-segment display. For simplicity, we'll assume that the 7-segment display is a common anode display, which is the |
| 40 | +default on Wokwi. |
| 41 | + |
| 42 | +## The chip JSON file |
| 43 | +Now we're ready to start writing code! We'll start from an empty ESP32-C3 project: |
| 44 | +[wokwi.com/projects/new/esp32-c3](https://wokwi.com/projects/new/esp32-c3). |
| 45 | + |
| 46 | +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 "+" |
| 47 | +button and serach for "Custom Chip". After selecting this option, type "sevseg-controller" for the chip name. Select |
| 48 | +the "C" language option. |
| 49 | + |
| 50 | +Wokwi will add a green breakout board for your custom chip, and create two new files in your project: |
| 51 | +- `sevseg-controller.chip.json` - defines the pinout |
| 52 | +- `sevseg-controller.chip.c` - defines the logic for the chip |
| 53 | + |
| 54 | +We'll start by editing the `sevseg-controller.chip.json` as follows: |
| 55 | +1. Change the `name` of the chip to "7 Segment Controller" |
| 56 | +2. Change the `author` of the chip to your name |
| 57 | +3. Change the `pins` array of the chip to include all the pin names: |
| 58 | + ```json |
| 59 | + ["VCC", "GND", "RX", "SEG_A", "SEG_B", "SEG_C", "SEG_D", "SEG_E", "SEG_F", "SEG_G"] |
| 60 | + ``` |
| 61 | + |
| 62 | +You'll see the green breakout board updating as you make changes to the JSON file. |
| 63 | + |
| 64 | +## Implementing the chip's logic |
| 65 | +Next, go to `sevseg-controller.chip.c`. This file implements the chip logic. The two important parts are: |
| 66 | +1. The `chip_state_t` struct - use it to store all the state information of your chip, together with all the objects |
| 67 | + that you create for your chips: IO pins, timers, etc. |
| 68 | +2. The `chip_init` function - Wokwi will call this function for every instance (copy) of your chip. The function should |
| 69 | + allocate memory for a new `chip_state_t` struct, initialize all the IO pins, the chip's state, and create any |
| 70 | + relevant objects (we'll see an example in a minute). |
| 71 | + |
| 72 | +The default implementation does not include any state or initialization - it only prints a message saying "Hello from |
| 73 | +custom chip!". You will see this message when you start the simulation - it'll appear in a new "Chips Console" tab |
| 74 | +below the diagram. |
| 75 | + |
| 76 | +We'll modify `chip_init` to perform the following actions: |
| 77 | +1. Initialize all the `SEG_x` pins as outputs |
| 78 | +2. Listen for UART data on the `RX` pin, and call a function that will update the `SEG_x` according to the character we |
| 79 | + received. |
| 80 | + |
| 81 | +### Initializing the 7-segment outputs |
| 82 | +Start by adding a `segment_pins` array to the `chip_state_t` struct. This array will store a reference to the `SEG_x` |
| 83 | +output pins, so we'll need to allocate 7 items: |
| 84 | +```C |
| 85 | +typedef struct { |
| 86 | + pin_t segment_pins[7]; |
| 87 | +} chip_state_t; |
| 88 | +``` |
| 89 | + |
| 90 | +Next, add the following code to `chip_init` (after the line that defines `chip`): |
| 91 | +```C |
| 92 | +chip->segment_pins[0] = pin_init("SEG_A", OUTPUT_HIGH); |
| 93 | +chip->segment_pins[0] = pin_init("SEG_B", OUTPUT_HIGH); |
| 94 | +chip->segment_pins[0] = pin_init("SEG_C", OUTPUT_HIGH); |
| 95 | +chip->segment_pins[0] = pin_init("SEG_D", OUTPUT_HIGH); |
| 96 | +chip->segment_pins[0] = pin_init("SEG_E", OUTPUT_HIGH); |
| 97 | +chip->segment_pins[0] = pin_init("SEG_F", OUTPUT_HIGH); |
| 98 | +chip->segment_pins[0] = pin_init("SEG_G", OUTPUT_HIGH); |
| 99 | +``` |
| 100 | + |
| 101 | +The code initializes each of the segment pins as an output, and sets the initial value to digital high. The 7-segment |
| 102 | +display has a common annode, so setting a segment pin high will turn that segment off. You can learn more about the |
| 103 | +`pin_init` function in the [GPIO API reference](../chips-api/gpio.md). |
| 104 | + |
| 105 | +### Listening to UART data |
| 106 | +Add the following code to `chip_init`, right after the code that initializes the segment pins: |
| 107 | +```C |
| 108 | +const uart_config_t uart_config = { |
| 109 | + .tx = NO_PIN, |
| 110 | + .rx = pin_init("RX", INPUT), |
| 111 | + .buad_rate = 115200, |
| 112 | + .rx_data = on_uart_rx_data, |
| 113 | + .user_data = chip, |
| 114 | +}; |
| 115 | +uart_init(&uart_config); |
| 116 | +``` |
| 117 | +
|
| 118 | +The code configures the `RX` pin as an input (in the third line), and sets up a `uart_config_t` structure. This |
| 119 | +structure configures the baud rate, as well as a function that will get called whenever there is new data, |
| 120 | +`on_uart_rx_data`. |
| 121 | +
|
| 122 | +Also note how we set `.user_data` to `chip` - this is important, as this value will be passed as a parameter to the |
| 123 | +`on_uart_rx_data` function, providing it access to our chip's state. |
| 124 | +
|
| 125 | +In our case, we are only interested in receiving data, so we set `.tx` to the special `NO_PIN` value. |
| 126 | +
|
| 127 | +:::tip |
| 128 | +
|
| 129 | +To learn more about using UART in Wokwi, check out the [UART API reference](../chips-api/uart.md). |
| 130 | +
|
| 131 | +::: |
| 132 | +
|
| 133 | +### From UART to 7-segment |
| 134 | +For the final part of the show, we'll implement the `on_uart_rx_data` callback. Paste the following code above the |
| 135 | +definition of `chip_init`: |
| 136 | +```C |
| 137 | +const uint8_t font[] = { |
| 138 | + ['0'] = 0b11000000, |
| 139 | + ['1'] = 0b11111001, |
| 140 | + ['2'] = 0b10100100, |
| 141 | + ['3'] = 0b10110000, |
| 142 | + ['4'] = 0b10011001, |
| 143 | + ['5'] = 0b10010010, |
| 144 | + ['6'] = 0b10000010, |
| 145 | + ['7'] = 0b11111000, |
| 146 | + ['8'] = 0b10000000, |
| 147 | + ['9'] = 0b10010000, |
| 148 | + ['A'] = 0b10001000, |
| 149 | + ['B'] = 0b10000011, |
| 150 | + ['C'] = 0b11000110, |
| 151 | + ['D'] = 0b10100001, |
| 152 | + ['E'] = 0b10000110, |
| 153 | + ['F'] = 0b10001110, |
| 154 | +}; |
| 155 | +
|
| 156 | +static void on_uart_rx_data(void *user_data, uint8_t byte) { |
| 157 | + chip_state_t *chip = user_data; |
| 158 | + uint8_t font_char = font[byte]; |
| 159 | + if (font_char) { |
| 160 | + for (int bit = 0; bit < 7; bit++) { |
| 161 | + uint8_t bit_value = font_char & (1 << bit); |
| 162 | + pin_write(chip->segment_pins[bit], bit_value ? HIGH : LOW); |
| 163 | + } |
| 164 | + } |
| 165 | +} |
| 166 | +``` |
| 167 | + |
| 168 | +This part simply defines the "font" - it maps between a character that we receive from UART and the corresponding |
| 169 | +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 |
| 170 | +it off. |
| 171 | + |
| 172 | +The `on_uart_rx_data` is where the actual magic happens. We use the `font` array to lookup the `byte` we received over |
| 173 | +UART. If we find a match (when `font_char` is not 0), we iterate over the bits of the `font_char`, and update each |
| 174 | +segment to its corresponding bit in `font_char`. |
| 175 | + |
| 176 | +That's it - we created a simple 7-segment controller chip for Wokwi! |
| 177 | + |
| 178 | +## Testing the chip |
| 179 | +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 |
| 180 | +the common pin of the 7-segment display to the 3.3V or 5V pin of the ESP32-C3 board! |
| 181 | + |
| 182 | +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 |
| 183 | +chip for good measures, even though the chip will be functional even without these pins. |
| 184 | + |
| 185 | +Finally, pase the following code into `sketch.ino`: |
| 186 | +```C |
| 187 | +void setup() { |
| 188 | + Serial.begin(115200); |
| 189 | +} |
| 190 | + |
| 191 | +int i = 0; |
| 192 | +void loop() { |
| 193 | + Serial.println(i, HEX); |
| 194 | + i++; |
| 195 | + if (i > 0xf) { |
| 196 | + i = 0; |
| 197 | + } |
| 198 | + delay(500); |
| 199 | +} |
| 200 | +``` |
| 201 | + |
| 202 | +It's a simple program that outputs all the hexadecimal values between 0 and F to the ESP32-C3's serial port - all the |
| 203 | +characters that are included in our custom chip's font. If you prefer plain C code, you can change the first line in |
| 204 | +`loop` to use `printf()` instead. |
| 205 | +```C |
| 206 | +printf("%X\n", i); |
| 207 | +``` |
| 208 | +
|
| 209 | +When you start the simulation, you should see the 7-segment display counting from 0 to F repeatedly. Hooray! |
| 210 | +
|
| 211 | +Doesn't work? No worries, here's the link to the final result, so you can compare it with yours: |
| 212 | +[wokwi.com/projects/371252876830114817](https://wokwi.com/projects/371252876830114817). |
| 213 | +
|
| 214 | +## Under the hood |
| 215 | +How do custom chips work? What happens with the C code you write? Behind the scenes, Wokwi takes this code and compiles |
| 216 | +it into a Web Assembly module using LLVM (if you are curious, here's the |
| 217 | +[Docker container](https://github.com/wokwi/wokwi-builders/blob/main/clang-wasm/Dockerfile) that does all the magic). |
| 218 | +
|
| 219 | +When you run the simulation, Wokwi creates an instance of the Web Assembly module, and calls `chip_init` once for |
| 220 | +every instance of the chip in your diagram. |
| 221 | +
|
| 222 | +Using Web Assembly means you can write your code in a variety of languages. Currently, only C is officially supported, |
| 223 | +there are some examples of how to write custom chips with Rust, AssemblyScript and even Zig. |
| 224 | +
|
| 225 | +There's even a hack where we use Custom Chips to simulate Verilog: we use Yosys CXXRTL to convert your Verilog code |
| 226 | +into C++, and then |
| 227 | +[use emscripten to compile](https://github.com/wokwi/wokwi-builders/blob/main/verilog-cxxrtl/project/compile.sh) |
| 228 | +the result along with [some glue code](https://github.com/wokwi/wokwi-builders/blob/main/verilog-cxxrtl/project/main.cpp) |
| 229 | +into Web Assembly. Scary cool? |
| 230 | +
|
| 231 | +## Next steps |
| 232 | +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 |
| 233 | +tutorial: |
| 234 | +
|
| 235 | +- **Fix the bug!** <br/> |
| 236 | + Unfortunately, our code has a bug - some values will cause it to display garbage on the 7-segment display (try sending |
| 237 | + it a 'b'). Some bound checking can help! |
| 238 | +
|
| 239 | +- **Add support for common cathode 7-segment displays** <br/> |
| 240 | + You can use an additional input pin to select between common anode/cathode, or use the [Attributes API](../chips-api/attributes.md) |
| 241 | + to allow the user to define the type of display by editing the chip attributes in `diagram.json`. |
| 242 | +
|
| 243 | +- **Add another communication protocol** <br/> |
| 244 | + You can turn our 7-segment display into an [I2C](../chips-api/i2c.md) or an [SPI](../chips-api/spi.md) device. |
| 245 | +
|
| 246 | +- **Support multiple digits** <br/> |
| 247 | + Control a two or four digital 7-segment display! Use the [Time API](../chips-api/time.md) to create a timer that will quickly |
| 248 | + alternate between the digits. |
| 249 | +
|
| 250 | +- **Add analog input** <br/> |
| 251 | + Use the [Analog API](../chips-api/analog.md) to read and display an analog input value. This makes the 7-segment controller chips |
| 252 | + useful even without a microcontroller - you can connect it directly to a potentiometer or an analog sensor, and |
| 253 | + display the reading directly. |
| 254 | +
|
| 255 | +- **Share your chip on GitHub** <br/> |
| 256 | + By sharing your chip's code on GitHub, you can make it easy for other users to include it in their project. You can |
| 257 | + use the [inverter-chip repo](https://github.com/wokwi/inverter-chip) as a starting point - it has a |
| 258 | + [GitHub action](https://github.com/wokwi/inverter-chip/blob/main/.github/workflows/build.yaml) that automatically |
| 259 | + compiles the chips and creates a release whenever you push a tag. |
| 260 | +
|
| 261 | + Here's an [example for a Wokwi project](https://wokwi.com/projects/350946636543820370) that uses this chip. Note the |
| 262 | + "dependencies" section in `diagram.json` - it tells Wokwi where to look for the chip implementation on GitHub. |
0 commit comments