уторак, 28. јануар 2020.

Yet another one day build of a digital clock

So, what is the difference this time? I have used TM1637 four-digit, seven-segment display, instead of driverlesss display. The difference is in the number of components. Instead of having twelve resistors and four transistors to drive the 7-segment display, now I have a single display with the built-in driver.

The code is on github.

Here is the schematics:
When you compare that to my previous 4-digit 7-segment build, you will notice the difference. The code is simpler, too:

const int CLK = 17; //Set the CLK pin connection to the display
const int DIO = 16; //Set the DIO pin connection to the display

TM1637Display display(CLK, DIO); //set up the 4-Digit Display.

void setup() {

  //set the display brightness  (0x0a is max)
  display.setBrightness(0x05); 

  ...

}

void loop() {
  
  display.showNumberDec(hour_1*1000 + hour_2*100 + minute_1*10 + minute_2);

  toggle = ~toggle;
  if (toggle)
  {
    display.showNumberDecEx(hour_1*1000 + hour_2*100 + minute_1*10 + minute_2, (0x80 >> 1), true);
  } 
    delay(500);
}

This is the clock:



петак, 24. јануар 2020.

Another One Day Build - Big Digital Clock

Welcome to another digital clock build. This time I have made a big digital clock for my friend. The clock was made using programmable LED strip, cut into small pieces which would act as segments in a big 7-segment digit.

The code is on github.

Programmable LED strip

This is a very nice thing - you can set any color to the individual LED bulb on the strip:

On the picture above you can see two segments, each having four LEDs. Each LED is programmable and can be set to any color of the 16 million colors available. I have purchased two meters of W2812B LED strip and cut it in segments having four LEDs. Then I have connected all segments with three wires and created digits:

Here you can see seven segments placed temporarily on the cardboard, for the testing purposes. Then I have connected the remaining three digits and two dots between hours and minutes:

I have recycled my previous code for the LED clock so I could now set and turn on/off individual digits of my new display. Here is the main loop:

void loop() {
  setDots(counter % 2);
  setDigit(0, hour_1);
  setDigit(1, hour_2);
  setDigit(2, minute_1);
  setDigit(3, minute_2);
  delay(1000);

  counter++;
  if (counter == 2)
  {
    counter = 0;
  }
}

The code above relies on getting the current time from the DS3231 RTC, separated into hour_1, hour_2, minute_1 and minute_2 variables. Then it is just matter of setting the corresponding segments on/off:

void setDigit(int digit, int value)
{
  int i, j;
  for (i = DIGIT_IDX_START[digit]; i < (DIGIT_IDX_START[digit] + 28); i++)
  {
    j = (i - DIGIT_IDX_START[digit]) / 4;                  // j is segment index
    if (DIGIT_MATRIX[value][j])
    {
       switch (state)
      {
        case RED:
          leds[i] = CRGB(255, 0, 0);
          break;
        case GREEN:
          leds[i] = CRGB(0, 255, 0);
          break;
        case BLUE:
          leds[i] = CRGB(0, 0, 255);
          break;
        case WHITE:
          leds[i] = CRGB(255, 255, 255);
          break;
        case RAINBOW:
          leds[i] = CRGB((i % 4 + 1) * 64, (i % 4 + 2) * 64, (i % 4 + 3)* 64);
          break;
        case BLACK:
          leds[i] = CRGB(0, 0, 0);
          break;
        default:
          leds[i] = CRGB(255, 0, 0);
      }
    }
    else
    {
      leds[i] = CRGB(0, 0, 0);
    }
  }
  FastLED.setBrightness(brightness);
  FastLED.show();
}

The FastLED library works by declaring an array of LEDS in the program. Here is my array of LEDs:

#define LED_PIN     2
#define NUM_LEDS    4*28

CRGB leds[NUM_LEDS + 2];

int DIGIT_IDX_START[] = {0, 28, 56, 84};
int DIGIT_MATRIX[][7] = {
// A     B     C     D     E     F     G  
{HIGH, HIGH, HIGH, HIGH, HIGH, HIGH, LOW},   // 0
{LOW,  HIGH, HIGH, LOW,  LOW,  LOW,  LOW},   // 1
{HIGH, HIGH, LOW,  HIGH, HIGH, LOW,  HIGH},  // 2
{HIGH, HIGH, HIGH, HIGH, LOW,  LOW,  HIGH},  // 3
{LOW,  HIGH, HIGH, LOW,  LOW,  HIGH, HIGH},  // 4
{HIGH, LOW,  HIGH, HIGH, LOW,  HIGH, HIGH},  // 5
{HIGH, LOW,  HIGH, HIGH, HIGH, HIGH, HIGH},  // 6
{HIGH, HIGH, HIGH, LOW,  LOW,  LOW,  LOW},   // 7
{HIGH, HIGH, HIGH, HIGH, HIGH, HIGH, HIGH},  // 8
{HIGH, HIGH, HIGH, HIGH, LOW,  HIGH, HIGH},  // 9
{LOW,  LOW,  LOW,  LOW,  LOW,  LOW,  HIGH},  // '-'  index is 10
{LOW,  LOW,  LOW,  LOW,  LOW,  LOW,  LOW}    // BLANK index is 11
};

I have an array of 4*28 + 2 LEDS for 4 digits and two dots. The DIGIT_IDX_START array holds the starting indices of all four digits in that array of LEDS. 

The final look is below. I have placed a white plastic board over LEDs to disperse the light and now it is quite nice to watch in all lighting conditions:


The button at the lower right corner turns on/off the display.

петак, 10. јануар 2020.

One Day Build - Digital Clock

I have recently decided to make a digital table clock. The design I chose consists of three important parts:
- ESP32,
- 7-segment four digits display with no special driver, and
- DS3231 real time clock.

The code is on github.

Since ESP32 has WiFi module, I used it for two purposes: to obtain the correct time once a day (and synchronize the DS3231 to that correct time), and to obtain the temperature information from my weather server, so I could get the outside temperature with a single button press.

I purchased the most simple 7-segment display which has 12 pins: 8 pins for the digit segment selection and 4 pins for the digit selection. Those four pins are common cathodes of the LEDs making each digit - that is why it has four common cathodes - for those four digits.

DS3231 is a good RTC with temperature compensation and optional battery (CR2032) to work without external power source.

Here is the schematics:


Driving the 7-segment four digit display is quite interesting. You cannot illuminate more than one digit in time. This means that you need to illuminate the first digit, then the second, then the third, and the fourth, and then to do it all again, fast. Thanks to our vision persistence, we get the impression that all four digits are illuminated. 

If you look at the schematics, you will notice that the digit definition is done using GPIO pins 32, 33, 25, 26, 27, 14, 12, and 2, which turn on/off segments a, b, c, d, e, f, g, and dot. That would only set the corresponding LEDs input to high or low voltage, but would not illuminate them (the circuit would not be closed until the common cathode is connected to the ground). To choose which digit will be illuminated (to have the common cathode connected to the ground), I used GPIO pins 16, 17, 18, and 19. Logical one (3.3V) on those GPIO pins would turn on the corresponding transistor and it would connect the common cathode of the connected digit to the ground.

// GPIO ports for LEDs
//           Dot A   B   C   D   E   F   G
int LEDS[] = {2, 32, 33, 25, 26, 27, 14, 12};

// GPIO ports for digits
int DIGITS[] = { 18, 19, 17, 16 };

Next I defined the matrix of LEDs for digit representation:

int DIGIT_MATRIX[][7] = {
// A     B     C     D     E     F     G  
{HIGH, HIGH, HIGH, HIGH, HIGH, HIGH, LOW},   // 0
{LOW,  HIGH, HIGH, LOW,  LOW,  LOW,  LOW},   // 1
{HIGH, HIGH, LOW,  HIGH, HIGH, LOW,  HIGH},  // 2
{HIGH, HIGH, HIGH, HIGH, LOW,  LOW,  HIGH},  // 3
{LOW,  HIGH, HIGH, LOW,  LOW,  HIGH, HIGH},  // 4
{HIGH, LOW,  HIGH, HIGH, LOW,  HIGH, HIGH},  // 5
{HIGH, LOW,  HIGH, HIGH, HIGH, HIGH, HIGH},  // 6
{HIGH, HIGH, HIGH, LOW,  LOW,  LOW,  LOW},   // 7
{HIGH, HIGH, HIGH, HIGH, HIGH, HIGH, HIGH},  // 8
{HIGH, HIGH, HIGH, HIGH, LOW,  HIGH, HIGH},  // 9
{LOW,  LOW,  LOW,  LOW,  LOW,  LOW,  HIGH},  // '-'  index is 10
{LOW,  LOW,  LOW,  LOW,  LOW,  LOW,  LOW}    // BLANK index is 11
};

To display a digit, you need to call two following functions:

void displayDigit(int value)
{
  int j, k;
  for (j = 1; j < 8; j++)
  {
    digitalWrite(LEDS[j], (DIGIT_MATRIX[value][j-1]));
  }
}

The function above will set the corresponding LEDs for the given digit, but will not illuminate anything, since we haven't selected which one of the four digits should be activated. To do so, we need the following function:

void activateDigit(int digit)
{
  switch(digit)
  {
    case 0:
      digitalWrite(DIGITS[0], HIGH);
      digitalWrite(DIGITS[1], LOW);
      digitalWrite(DIGITS[2], LOW);
      digitalWrite(DIGITS[3], LOW);
      digitalWrite(LEDS[0], LOW);
    break;
    case 1:
      digitalWrite(DIGITS[0], LOW);
      digitalWrite(DIGITS[1], HIGH);
      digitalWrite(DIGITS[2], LOW);
      digitalWrite(DIGITS[3], LOW);
      digitalWrite(LEDS[0], LOW);
    break;
    case 2:
      digitalWrite(DIGITS[0], LOW);
      digitalWrite(DIGITS[1], LOW);
      digitalWrite(DIGITS[2], HIGH);
      digitalWrite(DIGITS[3], LOW);
      digitalWrite(LEDS[0], LOW);
    break;
    case 3:
      digitalWrite(DIGITS[0], LOW);
      digitalWrite(DIGITS[1], LOW);
      digitalWrite(DIGITS[2], LOW);
      digitalWrite(DIGITS[3], HIGH);
      digitalWrite(LEDS[0], LOW);
    break;
    default:
      digitalWrite(DIGITS[0], LOW);
      digitalWrite(DIGITS[1], LOW);
      digitalWrite(DIGITS[2], LOW);
      digitalWrite(DIGITS[3], LOW);
      digitalWrite(LEDS[0], LOW);
  }
}

So, displaying time from the RTC module looks like this. First we read the time from the RTC (once in two seconds):

void read_time() 
{
  DateTime now = rtc.now();
  int hour = now.hour();
  int minute = now.minute();

  hour_1 = hour / 10;
  hour_2 = hour % 10;
  minute_1 = minute / 10;
  minute_2 = minute % 10;
}

We break up the time into four digits: hour_1, hour_2, minute_1, and minute_2. Then, in the main loop, we display that time by showing each digit quickly (each digit is illuminated one millisecond):

void loop()
{
  switch(counter)
  {
    case 0:
      displayDigit(hour_1);
      break;
    case 1:
      displayDigit(hour_2);
      break;
    case 2:
      displayDigit(minute_1);
      break;
    case 3:
      displayDigit(minute_2);
      break;
  }
  activateDigit(counter);
  counter++;
  if (counter == 4)
  {
    counter = 0;
  }

  // duty cycle adjustment for the LED brightness
  delayMicroseconds(500);
  activateDigit(-1);
  delayMicroseconds(500);
}

As you can see, the time is fetched from the RTC outside of the main loop. I have done that because obtaining the time from the RTC inside the loop might screw up the LED illumination, since the main loop would be blocked while reading the time from the RTC. To avoid that, I have used the ESP32 task to do the RTC reading. In the setup function, I have created a task for obtaining time:

// create a task that will be executed in the Task1code() function,
// with priority 1 and executed on core 0
// this task will get time from RTC every two seconds
  xTaskCreatePinnedToCore(
              Task1code,   /* Task function. */
              "Task1",     /* name of task. */
              10000,       /* Stack size of task */
              NULL,        /* parameter of the task */
              1,           /* priority of the task */
              &Task1,      /* Task handle */
              1);          /* pin task to core 1 */


Task1code() is a task function which is executed in parallel with the loop() function. Task1 is a task handler variable, which holds a handler for this task:

TaskHandle_t Task1;

void Task1code( void * pvParameters ){
  for(;;){
     read_time();
     delay(2000);
  } 
}

This task is executed as an infinite loop. It obtains the time from the RTC and then sleeps for two seconds. Then it does all of it again.

This is the time displayed by the clock:



Getting the outside temperature from my weather server was an interesting task. When I press the button, the clock would have to connect to the weather server, send the HTTP request for the temperature info, receive the HTTP response, parse it and display the outside temperature on the clock, instead of current time.

const char root_ca[]= {
// here comes the certificate in bytes...
};

http.begin("https://myserver/status", root_ca);

int httpCode = http.GET();

// httpCode will be negative on error
if(httpCode > 0) {
    // got the response from the server
    if(httpCode == HTTP_CODE_OK) 
    {
        String payload = http.getString();
        String t = parse_response(payload);
        int temp = t.toInt();
        if (t.startsWith("-"))
        {
          hour_2 = 10;  // "-" sign
          temp  = -temp;
        } else 
        {
          hour_2 = 11;  // BLANK character
        }
        hour_1 = 11;
        minute_1 = temp / 10;
        minute_2 = temp % 10;
    }
} else {
    Serial.printf("[HTTP] GET... failed, error: %s\n", http.errorToString(httpCode).c_str());
}

http.end();

The clock would display the temperature outside:



When I press the button again, the clock will display the internal temperature, obtained from the RTC. RTC has the temperature sensor built in so that it can compensate the time drift caused by the temperature change. That sensor info is exposed to the programmer:

float int_temp = rtc.getTemperature();
int temp = (int)int_temp;
temp = temp -2;
hour_1 = 11;
hour_2 = 11;
minute_1 = temp / 10;
minute_2 = temp % 10;

Here is the temperature inside:


How is this clock initialized when powered up? How is this clock synchronized to the precise time? I used the WiFi connection to connect the clock to the time server:

http.begin("https://myserver/time", root_ca);

int httpCode = http.GET();

// httpCode will be negative on error
if(httpCode > 0) {
    // got the response from the server
    if(httpCode == HTTP_CODE_OK) 
    {
        String payload = http.getString();
        parse_time(payload);
        rtc.adjust(DateTime(year, month, day, hour, minute, second));
    }
} else {
    Serial.printf("[HTTP] GET... failed, error: %s\n", http.errorToString(httpCode).c_str());
}

http.end();

The code above does not synchronize the clock to the exactly precise time (because of the delay), but it is good enough for me.

Conclusion

This table clock build was very interesting. The main challenge was to make all the digits show properly on the 7-segment display. When I mastered that, the rest was a breeze: getting the temperature from the weather server and showing the internal room temperature.

четвртак, 5. децембар 2019.

Bench power supply

I got inspired by this video:

The kit can be obtained for less than 10$, so you "only" need the rest of the stuff (case, better potentiometers, voltmeter, ampermeter, connectors, switches, etc.). I have purchased my kit from aliexpress (click on the image below):

I had a broken UPS, so I could reuse its case and transformer. I also had a cooler (heat sink with a fan) from my old processor, so I got this:

Here is a different point of view:

And, finally, here is the completed project (I have connected my Raspberry Pi 3):

For voltage regulation I have replaced the original potentiometer from the kit with the precise one, with multiple turns. That way I can set very precise voltage. For current limitation, I have left the original potentiometer. This power supply has the overcurrent protection and when it happens, the LED turns on. I have connected that LED connector to the UPS built-in LED.

Conclusion

This power supply is a linear one. It means that all the voltage difference between input and output  multiplied by current gets dissipated on a large transistor with the heat sink. It is not the most efficient way of regulating power, but for couple of bucks it is a nice project.

петак, 1. новембар 2019.

Fan control on the Raspberry Pi 4

I have recently purchased Raspberry Pi 4. It goes quite hot when working (even in idle), so I had to obtain a cooler. I have got one with big aluminum heat sink, and two small fans. They are meant to be connected to 5V and to work all the time. I didn't like that so I have decided to make a fan control.

The easiest way I could find was on this video:
https://www.youtube.com/watch?v=Pw1kSS_FIKk

The idea is quite simple: use one MOSFET to control the fan. I have connected the fan connector to the drain and GPIO pin to the gate and it looks like this:

The schematics of the circuit.

I have used IRF640N MOSFET, since I could not find the IRF530N, which was shown in the video. Here is the photo of the actual contraption:

MOSFET-based fan control

The software for the fan control is basic: turn on the fan if the temperature goes over the high threshold, and turn off the fan if the temperature goes lower than the low threshold.

#!/usr/bin/env python

import RPi.GPIO as GPIO
import time
import threading
import os
import sys

from subprocess import * 

# Fan control port (GPIO 23)
PORT_FAN = 23
HOT  = 55
COLD = 50
OFF = 0
ON  = 1
GPIO.setmode(GPIO.BCM)
GPIO.setup(PORT_FAN, GPIO.OUT)

GPIO.output(PORT_FAN, OFF)   # turn off the fan

def getTemp():
p = Popen("vcgencmd measure_temp|cut -c 6-7", shell=True, stdout=PIPE)
t = "" + p.communicate()[0]
t = t.strip()
t = int(t)
return t

while True:
temp = getTemp()
print temp
if temp > HOT:
GPIO.output(PORT_FAN, ON)   # turn on the fan
print "Turning the fan ON"
if temp < COLD:
GPIO.output(PORT_FAN, OFF)   # turn off the fan
print "Turning the fan OFF"
time.sleep(0.1)

GPIO.cleanup() 

 As you can see, the software is simple. If the temperature goes over 55 degrees of Celsius, the fans are turned on. When the CPU temperature goes below 50 degrees, the fans are turned off.

With this big aluminum heat sink, the idle CPU gets between 48 and 50 degrees.

One note: you can see on the photo above that I have connected +5V and GND of the power supply directly on the GPIO pins for +5V and GND, instead of using USB-C power cord. I have done that because I have quite decent 5V power supply which is not USB-C, so if I connect the USB-C cable to it, the additional voltage drop happens across that cable. Lower the quality of the cable, bigger voltage drop becomes.



субота, 5. октобар 2019.

Flashing DE0-NANO FPGA board and using DEV_CLRn reset functionality

In this post I am going to talk about programming DE0-NANO FPGA board two ways:
1. temporary programming, meaning that the design will not survive powering off, and
2. permanently storing (flashing) the design, so it will survive power off.

I will also address the idea of a mega-reset using the DEV_CLRn feature at the bottom of this post.

Disclamer: You are doing all of this at your own risk. I am not responsible for any problem caused by these examples. To prevent problems, check the documentation of your DE0-NANO to see if you have the same type of Programmer and EEPROM chips.

1. Temporary programming

Whenever you compile your design at the Quartus II IDE, you can send the design to the FPGA board via Programmer:

1. double click on the Program Device in the table:

2. When the Programmer opens, look for the USB blaster right to the Hardware Setup... button:


3. If you see the "No Hardware" text, click on the Hardware Setup... button. That would open the Hardware Setup dialog:

4. Double click the USB Blaster and Close. The Programmer should look like this:


5. Now the USB Blaster is present right to the Hardware Setup... button.

6. Click on the Start button to send the design to the FPGA board.

All this is temporary, meaning that the design will be erased when you power off the board.

Alternative way of temporary programming

An alternative way of doing this is by executing the following program:

C:\altera\13.0\quartus\bin\quartus_pgm.exe

You need to supply that program with the following command line parameters:

-c usb-blaster -m jtag -o P;<path_to_the_SOF_file>

That could be typed like this:


C:\altera\13.0\quartus\bin\quartus_pgm.exe -c usb-blaster -m jtag -o P;<path_to_the_SOF_file>

I have done that in my FPGARaspbootin program (the Run FPGA manually button, and Auto Run FPGA check box):


2. Permanent programming (flashing)

To flash the DE0-NANO device, you need to:

1. convert the SOF file into the JTAG Indirect Configuration File (*.jic file), by choosing File -> Convert Programming Files... menu option. That would open the Convert Programming File Dialog.

2. Choose the Programming file type to:
JTAG Indirect Configuration File (*.jic file)

3. Open the Configuration device combo box and choose: EPCS64

4. Click on the Flash Loader in the bottom table and click on the Add Device... button. That would open the Select Devices dialog. Choose Cyclone IV E and EP4CE22:


5. Click on the SOF Data in the table and click on the Add File... button. Choose your SOF file:

6. Click on the chosen SOF file in the table (in my example, the computer.sof file) and click on the Properties. Turn on the compression:

7. After that start the Programmer, delete the SOF file (if existed), and add the JIC file (click on the Add File... button). Make sure that both Program/Configure checkboxes are turned on:

8. Click on the Start button and wait for the 100% at the progress bar (upper right corner).

9. You can power off and then power on the FPGA and it will still have the design in it.

Reset all registers with the Reset button (DEV_CLRn option)

I have experienced some strange behavior regarding resetting my design. I have made my KEY[0] clear all of my registers using the Verilog code. That design, however, failed somehow to completely reset my board. Simply, after the reset, the design would behave unreliable. I have managed to solve this problem by introducing the mega-reset feature. Here is the explanation:

In DE0-NANO, the KEY[0] is connected to the PIN_J15, which is in turn connected to the DEV_CLRn pin. This can be used to reset all registers in the FPGA when you press the KEY[0] key on the DE0-NANO board. That is a kind of mega-reset. However, this feature is turned off by default. You need to enable it. Here is the procedure:

1. right click on the Cyclone IV... in the Project Navigator and choose the Device... option:

2. click on the Device and Pin Options... button:

3. Find and enable the Enable device-wide reset (DEV_CLRn) check box:

4. Open the Pin Planner dialog (Assignments -> Pin Planner menu option), and change the KEY[0] from PIN_J15 to something unassigned, like PIN_G5 (it is unused in my project):

After that, whenever you press the KEY[0] key, you will reset your entire board.

среда, 18. септембар 2019.

Added new VGA graphics mode

This is a follow-up of my original FPGA computer post.

FPGA computer has got a new VGA mode: 640x480 in two colors. One byte of the video memory holds 8 pixels, each being 1 or 0 (white or black):

Pixel 7
Pixel 6
Pixel 5
Pixel 4
Pixel 3
Pixel 2
Pixel 1
Pixel 0

If you want to put four white and four black pixels at the top left corner of the screen (from the (0,0) to the (7,0) coordinates), you need to type:

mov.w r0, 0xF0
st.b [1024], r0

This mode is made out of existing VGA text mode, since it does almost all the job. The text mode shows characters made of 8x8 pixels on the 640x480 VGA screen. I have inserted additional Verilog code inside the text mode module, in a way that when the 640x480x2 mode is set, it shows the pixels, not the characters.

First of all, the programmer needs to set the display mode to 640x480x2:

mov.w r0, 2
out [128], r0

The VGA module detects this and changes the signal generation on the VGA connector:

if (valid) begin
  if (vga_mode == 0)  begin
    r <= inverse ^ (pixels[7 - (x & 7)] ? !curr_char[6+8] : curr_char[2+8]);
    g <= inverse ^ (pixels[7 - (x & 7)] ? !curr_char[5+8] : curr_char[1+8]);
    b <= inverse ^ (pixels[7 - (x & 7)] ? !curr_char[4+8] : curr_char[0+8]);
  end
  else if (vga_mode == 2) begin
    r <= inverse ^ (curr_char[15 - (x & 15)]);
    g <= inverse ^ (curr_char[15 - (x & 15)]);
    b <= inverse ^ (curr_char[15 - (x & 15)]);
  end
end 

What we see above is the Verilog code that sets the R, G and B wires of the VGA connector to the corresponding values, depending of the display mode. If the mode is text (vga_mode == 0), it outputs the font pixels of the character that was found in the video memory (pixels module returns actual pixels of the current_char register). However, if the mode is graphics (vga_mode == 2), then the actual byte found in the video memory is outputted to the wires (actual bits of the byte are pixels).

All this means is that current_char register holds two bytes from the video memory, and it is periodically loaded from the video memory. At the beginning, it is loaded from the very first word of the video memory, and after that VGA module loads two bytes of the video memory periodically:
- at the end of each character in text mode, it fetches the next character,
- at the end of each 16 pixels of the graphics mode, it fetches next 16 bits (pixels),
- at the end of each scan line it fetches the content of the beginning of the next scan line,
- at the lower right corner scanline end, it fetches the content of the top left corner.

This is the photo of the actual monitor:

And, this is the screenshot of the emulator:


Conclusion

Before this feature was introduced, the FPGA computer had two video modes:
- text 80x60 mode characters text mode (made of 640x480 pixels and each character is 8x8 pixels in size).
- graphics 320x240 mode, each pixel being in one of 8 colors.

This new mode is added to the existing VGA text module (80x60 characters) since that module already works with 640x480 pixels. The only additional thing was to show pixels, not characters. So, the computer now has one more mode: 640x480 in two colors (black and white). You can look at the Verilog code:

https://github.com/milanvidakovic/FPGAComputer32/blob/master/vga_module.v

And, you can look at the assembly code which draws everything here:

https://github.com/milanvidakovic/Assembler32/blob/master/raspbootin/graphics640.asm