Thursday, 2 April 2026

51 Two Test programs for a GPS, ublox NEO-M8U @05/4/26

Having purchased a cheap Ublox GPS, I wired it up to a TTL RS232<->USB convertor, downloaded U-Center from Ublox and observed incoming packets at (the power up default) of 9600 Baud. As I wanted to add this to an Arduino uno (Adafruit Metro actually) and keep the Arduino's sole hardware UART for the convenience of using the Arduino IDE and the USB monitor, it was important that the GPS baud rate was low enough to use the "SoftSerial" library to allow a software UART to read data reliably. 

My USB-TTL-RS232 modules were 15 years old, but the data sheet is at RS Stock No.:429-278


Wiring 3 wires up and using tera-term saw data coming out at 9600 baud. Downloading U-Center from ublox ( at https://www.u-blox.com/en/product/u-center allowed further experimentation, I did not update firmware or alter the GPS configuration, if it works, don't fix it...

Actually at a later date I may try and increase the update rate and a future application may want the 1pps output signal to be increased if possible. (for a GPS disciplined oscillator)

One feature of the U-center software is a deviation map, shown below, by the way it took many many minutes to get a cold start, but the U-Center software did show more and more satellites coming into view and their status changing from blue to green (I assume that's a good thing) I will test warm start tomorrow!




Anyway, the Arduino code I used was simply the Software Serial example with one print line commented out. Here it is, I wired the GPS TX pin to pin 10 on the Arduino and the RX pin to pin 11 (although it is not used here)

/*
  Software serial multiple serial test

 Receives from the hardware serial, sends to software serial.
 Receives from software serial, sends to hardware serial.

 The circuit:
 * RX is digital pin 10 (connect to TX of other device)
 * TX is digital pin 11 (connect to RX of other device)
 */
#include <SoftwareSerial.h>

SoftwareSerial mySerial(10, 11); // RX, TX

void setup() {
  // Open serial communications and wait for port to open:
  Serial.begin(9600);
  while (!Serial) {;}
  mySerial.begin(9600);
}//--end setup--//

void loop() { // run over and over
  if (mySerial.available()) {
    Serial.write(mySerial.read());
  }
  if (Serial.available()) {
    mySerial.write(Serial.read());
  }
}//--end loop--//
////////////////////////
Here is the output from the arduino IDE serial monitor

The code above just passed the GPS data to the USB serial port of the PC.
Running a terminal emulator such as TeraTerm will show the data if set to
correct BAUD rate.

The next arduino program only outputs lines that contain RMC data. It also
strips the first few characters and some characters at the end that carry
un-needed data. This makes it easier to use the RFM69 radio transceivers.
As these have a packet limit of 60 bytes.

/////////////////////////////////////////////////////////////////////
//
// Reads part of an RMC line from a GPS wired to pins 10 and 11
// using a SoftwareSerial routine. It transmits it to the PC
// using a hardware UART to USB
//
#include <SoftwareSerial.h>

SoftwareSerial SSer(10, 11); // RX, TX
char buf[80];

void setup() {
  // Open serial communications and wait for port to open:
  Serial.begin(9600);
  while (!Serial) {;}
  // set the data rate for the SoftwareSerial port
  SSer.begin(9600);
}//-- end setup--//

void loop() { // run over and over
  int b,i;
  do{
    if (SSer.available()) {
        b=SSer.readBytesUntil('\n',buf,sizeof(buf)-1);
        buf[b-14]='\0';// make truncated string
    }
  }while(buf[3]!='R'||buf[4]!='M'||buf[5]!='C');//--end do--//

  // Have got shortened RMC packet, up to course data xxx.x
// move it up 6 bytes (overwrite first six, unneeded data bytes
  for( i=0;i<b-6;i++)buf[i]=buf[i+6];// move array left
  buf[i]='\0'; // marks the end of the string
  Serial.println(buf);
}//--end loop--//




Tuesday, 31 March 2026

50 Experimenting with Python and TKinter talking to an Arduino@5/4/26

We have a need to send two numbers to a remote Arduino and receive a complex text string from its GPS.

I decided to use the arrow keys on the PC keyboard, the right and left arrows increment and decrement one of the numbers from 0 to 99 (might change). The up and down arrow keys increment and decrement the other number. These will eventually control Rudder and Sail position on a model yacht, using two servos connected to an Arduino on the boat. It is connected to a RFM69HCW radio transceiver (obtained from Pimoroin.co.uk, it is sold by Adafruit in the states). A PC on the shore or support boat is wired to another Arduino and that Arduino also has an RFM69HCW radio transceiver. The boat Arduino also has a GPS module. Future extensions will add wind sensors etc.,

To get a very basic working system I decided to use the Tkinter graphics system that is supplied with Python. This allows a basic crude graphic user interface (GUI) that looks dated now. There are extensions to Tkinter and there are other graphic libraries available, but I decided to keep it simple. 

In addition there are a number of ways to put Tk graphical objects (known as widgets) on a main window. You can specify a .pack method or a .grid method. Most books will guide you through these, or you can just copy code and experiment. However, I used a much simpler system that uses a .place method.

The .place method is passed an x and a y value and the widget is simply placed there. This works fine until you try to resize the main window. It is also a bit tedious as you have to plan and work out where you want everything to line, I do not consider that onerous. And I don't need resizable windows.

Before presenting code let me say that I am cherry picking bits of code and ignoring others, I use a minimum of widgets, just enough to get the job done, it could be prettier; simpler is more educational, it can be improved once it is working. I like this strategy.

I use one main window, I call it master, it could have been named anything, many books call the first window root. I create a second window totally within the master window, this is of type canvas which is a window which allows drawing graphical objects such as thick lines and rectangles. I also use labels and I use the .bind method to attach a function to the keys that I want to trigger an action.

That should make the code make some sense!

Also of note is that Tkinter (and most of the other GUI systems) use a different way of working from simple code. It is event driven programming. You are used to simple code where the execution flows down the page, one statement after another, with the odd loop and jump to control the flow of control.

TKinter programs initially look a bit funny, the code moves down the page but then there is a final function call. In traditional programming terms this function is entered and NEVER leaves. You might think the code would just freeze but what actually happens is that any widget defined before that final call has a number of methods attached to it and the final call creates a list of possible events and as each event happens (such as you clicking a mouse or hitting a key) that final function calls the appropriate method. After it returns you are back in that final function again. It is running an event scheduler. You can supply your own functions and these get called if they are defined as callback functions by the appropriate widget. It will make more sense once you run the program.

A further complication is that the widgets can use variables of several types but these are derived from the normal types of variable. So, whilst we might set the text attribute of a widget to some fixed text, if we want to have a string that changes we have to (a) use the textvariable attribute, not the text one. and (b) we have to use a type known as stringvar, not string. We assign values to stringvar variables using the .set(value) method and we read values out of stringvars using the .get() method. Watch out for this as it took me a while to realise this. There are also IntVars etc., for other derived types.

Here is the code, with a few lines commented out that interface with the Arduino through the PySerial library and the USB UART connected to the Arduino. With these lines commented out you can run the python code in isolation. Note I have "hardwired" the code to use COM6, you will need to change this eventually to suit your system.

# crude graphic program to allow sending rudder and sail positions to
# an arduino connected to port COM6, adjust port to suit and re run
# The comms code blocks, first we send 2 numbers as 4 chars
# the code freezes until a string is received, no timeouts are set
#
# Testable without comms by commenting out function write_read and
#          and gps.set line around line 74 and port open code, line 96
#######################################################################
from tkinter import *
from tkinter import ttk
from math import *

import serial
import time
############################## FUNCTION DEFINITIONS ##################
'''def write_read(x,y):
    arduino.write(str(x).encode()) # sends utf-8, chars as bytes
    arduino.write(str(y).encode())
    arduino.write('\n'.encode())
    time.sleep(0.05)
    data=arduino.readline().decode().strip()
    return   data # string 
'''
def keypress(event): # we bind the arrow keys, rest ignored here
    """Receive a keypress and move the Servos by a specified amount"""
# first undraw Rudder and Sail
    x1=200
    y1=260
    x2=x1+50*sin((Rudder.get()-50)/100.0)
    y2=y1+50*cos((Rudder.get()-50)/100.0)
    canvas.create_line(x1,y1,x2,y2,fill='white',width=10)
    
    x1=200
    y1=60
    x2=x1+150*sin((Sail.get()-50)/100.0)
    y2=y1+150*cos((Sail.get()-50)/100.0)
    canvas.create_line(x1,y1,x2,y2,fill='white',width=6)
    
    
    if event.keysym == 'Up':
        Sail.set(Sail.get()+1)
        if Sail.get() == 100:
            Sail.set(99)
    elif event.keysym == 'Down':
        Sail.set(Sail.get()-1)
        if Sail.get() < 0:
            Sail.set(0)        
    elif event.keysym == 'Right':
        Rudder.set(Rudder.get()+1)
        if Rudder.get() == 100:
            Rudder.set(99)  
    elif event.keysym == 'Left':
        Rudder.set(Rudder.get()-1)
        if Rudder.get() < 0:
            Rudder.set(0)
    else:
        pass
    
# Undraw and Draw Rudder on Canvas, rudder is 0 to 99, 50 = 50%
    x1=200
    y1=260
    x2=x1+50*sin((Rudder.get()-50)/100.0)
    y2=y1+50*cos((Rudder.get()-50)/100.0)
    canvas.create_line(x1,y1,x2,y2,fill='red',width=3)
# Undraw and Draw Sail position on Canvas, 0 to 99, 50 = 50%       
    x1=200
    y1=60
    x2=x1+150*sin((Sail.get()-50)/100.0)
    y2=y1+150*cos((Sail.get()-50)/100.0)
    canvas.create_line(x1,y1,x2,y2,fill='green',width=3)
    
    canvas.create_rectangle(175,10,225,260,width=1)#redraw boat

# now send out to arduino and return GPS string

    gps = StringVar()
#    gps.set( (write_read(int(Rudder.get()),int(Sail.get()))).ljust(40," "))
    gps.set("42.42424N,005.42424W,000.0,000.0")
    Label(master,text="Sending").place(x=150,y=40)
    Label(master,textvariable=Rudder).place(x=250,y=40) 
    Label(master,textvariable=Sail).place(x=300,y=40)
    
    Label(master,text="String from GPS").place(x=150,y=60)
    Label(master,textvariable=gps).place(x=250,y=60) 

############################# MAIN CODE BEGINS #######################
master = Tk()
master.geometry("600x600")

Port = StringVar()
Port.set("COM6") 

Label(master, text = "Using Port ").place(x=1,y=10)
Label(master, textvariable = Port).place(x=60, y=10)

####arduino = serial.Serial(port=Port.get(),baudrate=9600)
    
# setup rudder and sail controls to arrow keys
Rudder = IntVar(master,50)
Sail = IntVar(master,60)

master.bind('<Up>',keypress)
master.bind('<Down>',keypress)
master.bind('<Left>',keypress)
master.bind('<Right>',keypress)

Label(master, text = "Rudder Position = ").place(x=175,y=10)
Label(master, textvariable = Rudder).place(x=275, y=10)

Label(master, text = "Sail Position = ").place(x=325,y=10)
Label(master, textvariable = Sail).place(x=405, y=10)

# paint graphic of boat here...

canvas=Canvas(master,width=400,height=400,background='white')
canvas.place(x=100,y=100)
canvas.create_rectangle(175,10,225,260,width=1) #draw boat

mainloop() # infinite loop, stringvars, widgets & events update ok 

#################################### code ends #######################

Running the program causes the screen below to appear - after you hit any of the
arrow keys, the left and right arrows affect the rudder and the up and down
arrows affect the sail position.


Click on the [x] in the top righthand corner to exit.

I will post a very very well commented version here soon that will explain the code line by line to beginning Python programmers.

49 Arduino driving RFM69HCW radio transceivers

 I recently bought a couple of radio transceivers to allow slow speed transfer of data between two Arduinos. I used the RFM69HCW $10 modules from Adafruit. These do not use the LoRa protocol but a simpler packet protocol that suited my application. Point to Point.

Here in the UK I bought them from https://shop.pimoroni.com/products/adafruit-rfm69hcw-transceiver-radio-breakout?variant=19594984071 at just under a tenner, I choose the 433MHz versions as it is easier to make antennas for 70cms and I have test equipment that allows tuning such antennas.

A declared power of 100mW and range of 500m sounded good. The data sheet says 50mA (+13 dBm) to 150mA (+20dBm) current draw for transmissions, ~30mA during active radio listening. The range figure is for line of sight and depends on the antennas. My application will have a PC linked to an Arduino on shore and a mobile model boat on water, the shore station can use a Yagi or decent antenna even if the boat only uses a simple quarter wave whip.

The tutorial on the Adafruit website shows it wired to their own metro boards, I had a couple of Seeduino Nano modules and used them. Their +5 volt output can source 200mA and can power the module directly. There are Arduinos that have limited power supply outputs from the internal 5 volt regulator so you might need to power the transceivers separately in some applications.

There are two Arduino libraries that support these modules, one from LowPowerLabs and one from RadioHead. I had a lot of bother initially when I was using the LowPowerLabs libraries but have since concluded it was probably not the fault of the library. I moved on to the RadioHead Libraries as Adafruit had a modifed version in their Github account. 

Their tutorial is at https://learn.adafruit.com/adafruit-rfm69hcw-and-rfm96-rfm95-rfm98-lora-packet-padio-breakouts and the software they recommend is at https://github.com/adafruit/RadioHead 

Be very careful! the current RadioHead library at their site is version 1.145 and the Arduino IDE keeps wanting to update your IDE to this version. RESIST THE TEMPTATION TO UPDATE THE LIBRARY when a pop-up appears in your IDE. The AdaFruit version is based on version 1.121.

The Adafruit has many more examples than the up to date version, use the Adafruit version and DO NOT UPDATE it. Or at least, I know if you use the Adafruit one the code below will work, by all means, once you have a working system, change libraries, test it and let me know. I just wanted to get a basic working system and then move on (to write a Python Tkinter program to interface to the arduinos)

The Adafruit library can be downloaded as a .ZIP file from their github account. You can easily install a Zipped library into your Arduino IDE installation by copying it to your libraries folder. The Libraries folder is in your Sketch folder. By default in your Documents folder although in my installation I have moved mine to a folder on the desktop, if you move your sketch folder the libraries folder moves with it.

Place your downloaded .zip into the libraries folder and start the Arduino IDE, select the top line menu item "Sketch"->Include Library>Add  .Zip Library. Do not use the new built in library manager icon on the left of the IDE.

I initially used the simple examples you get when you install the library but couldn't get them working, I tried several different wirings and then started to look at other examples. Some had code to pulse the reset line, some examples didn't even wire up the reset pin on the module. I suspect that on some Arduinos the i/o lines come up with a rise time that allows the module to reset itself on powerup. In any case by modifying the "feather arduino" example and adding reset code and wiring I was able to get it going. YMMV.

Here is my wiring;

As you can see the SCK,MOSI and MISO pins goto the Arduino SPI pins. The CS select pin and G0 pin (interrrupt) go to suitable digital pins and are specified when you create the RH_RF69 object from the RF69 class. The RST reset pin is just wired to a digital pin that you make an output. 

I stripped the code down to the minimum and it is listed below. 

Note the "tx" code calls .send and then .waitPacketSent before using .waitAvailableTimeout to wait for data which is picked up with a .recv call. Just for interest I pickup the Received Signal Strength Indication (RSSI) to see how well the antennas are working and range tests (yet to done). The method .lastRssi returns a number in dB (I think). Setup uses the methods .init, .setFrequency, .setTxPower and .setEncryption

Note the receive code has similar setup code and its loop section calls .available to see if data has arrived. The .recv method gets the data into a buffer and I, again, read the signal strength of the last received packet with .lastRssi. A quick .send and .waitPacketSent gives a handshake back to the transmitter code to repeat the cycle

If you look into the .zip library and read the .h and .cpp files you can see the documentation, or at least a list of the various methods (functions) available to you.

// From Feather RawDemo Tx

#include <SPI.h>
#include <RH_RF69.h>  //Version 1.121 has more examples than later, don't update for now!!!
#define RF69_FREQ 434.0
#define RFM69_CS     10  // "B"
#define RFM69_INT    2 // "C"
#define RFM69_RST    6  // "A"
uint8_t rudder_position=49;// pick up new values from PC
uint8_t sail_position=48;
RH_RF69 rf69(RFM69_CS, RFM69_INT); // create Object
void setup() {
  Serial.begin(115200);
  pinMode(RFM69_RST, OUTPUT);
  digitalWrite(RFM69_RST, LOW);delay(10);// slow reset
  digitalWrite(RFM69_RST, HIGH);delay(10);
  digitalWrite(RFM69_RST, LOW);delay(10);
  if (!rf69.init()) {Serial.println("RFM69 radio init failed");while (1);}//hang
  Serial.println("RFM69 radio init OK!");
  if (!rf69.setFrequency(RF69_FREQ)) {Serial.println("setFrequency failed");}
  rf69.setTxPower(20, true);// 2nd argument must be true for RFM69HCW
 
  // The encryption key has to be the same as the one in the server
  uint8_t key[] = { 0x04, 0x02, 0x04, 0x02, 0x04, 0x02, 0x04, 0x02,
                    0x04, 0x02, 0x04, 0x02, 0x04, 0x02, 0x04, 0x02};//IMcC chose..
  rf69.setEncryptionKey(key);
}
void loop() {
  delay(2000);// don't flood the receiver, could make bigger...
  char radiopacket[7] = "r00s00";// Rudder and sail position
  // Pick up variables from PC here, test with 00 <<<<<
  rudder_position=51;// test
  sail_position=61;// test
  itoa(rudder_position, radiopacket+1, 10);// overwrite first 00, base 10
  itoa(sail_position, radiopacket+4, 10);// overwrite 2nd 00, base 10
  Serial.println(radiopacket);
  // Send a message!
  rf69.send((uint8_t *)radiopacket, strlen(radiopacket));
  rf69.waitPacketSent();
  // Now wait for a reply
  uint8_t buf[RH_RF69_MAX_MESSAGE_LEN];
  uint8_t len = sizeof(buf);
  if (rf69.waitAvailableTimeout(500)) {
    // Should be a reply message for us now
    if (rf69.recv(buf, &len)) {
      Serial.println((char*)buf);
      // Should be GPS position, pass on up to PC
      Serial.print("RSSI: ");Serial.println(rf69.lastRssi(), DEC);
    } else {
      Serial.println("Receive failed");
    }
  } else {
    Serial.println("No reply");
  }
}
Here also is the code for a suitable receiver

// From Feather RawDemo RX
#include <SPI.h>
#include <RH_RF69.h> //Version 1.121 has more examples than later, don't update for now!!!

#define RF69_FREQ 434.0
#define RFM69_CS     10
#define RFM69_INT    2
#define RFM69_RST    6

uint8_t rudder_position=49;
uint8_t sail_position=48;
uint8_t buf[RH_RF69_MAX_MESSAGE_LEN];
uint8_t len = sizeof(buf);

RH_RF69 rf69(RFM69_CS, RFM69_INT); // create object

void setup() {
  Serial.begin(115200);

  pinMode(RFM69_RST, OUTPUT);
  digitalWrite(RFM69_RST, LOW);delay(10);// slow reset
  digitalWrite(RFM69_RST, HIGH);delay(10);
  digitalWrite(RFM69_RST, LOW);delay(10);

  if (!rf69.init()) {Serial.println("RFM69 radio init failed"); while (1);}// hang
  Serial.println("RFM69 radio init OK!");
  if (!rf69.setFrequency(RF69_FREQ)) {Serial.println("setFrequency failed");}
  rf69.setTxPower(20, true);// 2nd argument must be true for RFM69HCW

  // The encryption key has to be the same as the one in the server
  uint8_t key[] = { 0x04, 0x02, 0x04, 0x02, 0x04, 0x02, 0x04, 0x02,
                    0x04, 0x02, 0x04, 0x02, 0x04, 0x02, 0x04, 0x02};//IMcC chose..
  rf69.setEncryptionKey(key);
}

void loop() {
 if (rf69.available()) {
    // Should be a message for us now
    if (rf69.recv(buf, &len)) {
      if (!len) return;// if zero no data
      buf[len] = 0;// terminate string
      rudder_position=(buf[2]-'\0')*10+(buf[2]-'\0');
      sail_position=(buf[4]-'\0')*10+(buf[5]-'\0');
      // now have two decimal numbers 0 to 99
      // how do you print numbers in the arduino IDE?



      Serial.print("RSSI: ");Serial.println(rf69.lastRssi(), DEC);
      // call function here to get position, course & speed<<<<
      strcpy(buf, "55.1234N,005.4321W,045,12");
      rf69.send(buf, sizeof(buf));
      rf69.waitPacketSent();
      Serial.println("Sent a reply");
    } else {
      Serial.println("Receive failed");
    }
  }// fi .available
}//--end loop--//


------------ ends as of March 2026 -----------------




Monday, 11 August 2025

48 - How hot does a heat sink get?

When designing electronic circuits you need to take into account the temperature of the components, if there is too much heat they will fail, if they run hot their lifetime is reduced. I once did reliability calculations for a friend to show how how much a temperature of 95 degrees Celsius reduced the lifetime of a complete electronic system, the notional "Mean Time To Failure" (MTTR) reduces exponentially as temperature goes up, the Arrhenius expression gives an indication of this, but it is not the complete story.

 A common rule of thumb is that for every 10°C increase in temperature, the failure rate of a component doubles, but this is conservative as it does not take into account all the reasons for failure of electronic systems. but even so, clearly going from 25 degrees to 55 degrees causes failure at least  2^3 or 8 times quicker.

Temperatures above 50 degrees should be avoided, 50 is about the maximum you can put your finger on (for about 5 seconds) without too much "ouch". A modern thermal imager is a useful tool. It is only accurate if the emmissivity of what is being looked at it is not too extreme.

The traditional way to calculate the temperatures is to use thermal resistance figures and add these up - the same way you add resistors in series. For semiconductors (the most sensitive of our components I think, at least for rapid failure, some capacitors do dry out at elevated temperatures) you want the temperature inside the transistor or diode to be kept below the manufacturers maximum and you increase lifetime if you can get well below it. The manufacturer hopefully publishes the thermal resistance from the semiconductor junction to the case. If this is all that is used them you can calculate what temperature the junction is if the case is at a certain value and the device is dissipating so many watts of power. Thermal resistances are the most convenient to work with, some datasheets give the data in different ways, derating curves or values, you can convert these to thermal resistances or, at worst, use data for the same physical package using a datasheet for a similar device

For example: the datasheet for the IRF510 states thermal resistance ratings as;

    Junction-to-ambient RthJA as 62 degrees C per watt  (this is for when no heatsink is used)

    Junction-to-case RthJC of 3.5 degrees/watt. (used when the case is not just in air)

    Case-to-(Heat)sink, flat greased surface RthCS of 0.5 degrees per watt 

(Note you can grease the interface, insert mica washers for electrical insulation but good heat transfer or use thermal pads, all have values well under a half a degree per watt)

An example heatsink costing £3 to £4 and two inches high, an inch and half wide and half an inch deep has a specified natural (non fan assisted) thermal resistance of 9 degrees C per watt (FEC1892328). For around a tenner you can get 3 or 4 degrees per watt heatsinks. Bigger is expensive.

The device has a maximum junction temperature of 175 degrees C (though I'd prefer to opt for 125)

The temperature rise above ambient is simply the power times the thermal resistance

Thus if the device is running at 10 Watts and we assume an ambient temperature of 25 degrees we have;

              Pd * (Rjc + Rcs + Rsa) + Ta  =  Tj  

              = 10 * (3.5 + 0.5 + 9 ) + 25   =  155  ignoring dissipation from case directly to air.

Fairly hot! and if the heatsink is inside a case that is poorly ventilated the "ambient" can easily be tens of degrees above 25. A fan or bigger heatsink is needed.

Big heat sinks are expensive, Harry Lythall (SM0VPO) has good rules of thumb on his website suggesting that if you take the square area of aluminium exposed to air then the thermal resistance of home made heat sinks is roughly 


Taken from http://sm0vpo.altervista.org/begin/heat-0.htm 

We assumed constant power being dissipated in the example above, so you'd have to use the maximum power as the worst case, if the device was being switched and was only "on" 10% of the time then you could guesstimate that the effective power was a tenth of the peak, but only if the cycle time was fast.

If the device is passing an SSB modulated carrier then a "duty cycle of 30% might be appropriate, which drops to 15% if you only transmit half the time. This assumes you don't talk for too long before saying "over". A large transmitter heatsink and case does have significant thermal mass which helps.

The ARRL says; (ignoring the ratio of receive to transmit times)

Conversational SSB with no speech processing, uses a 20% duty cycle which includes voice characteristics and syllabic duty factor.

Conversational SSB with heavy speech processing, uses a 50% duty cycle which includes voice characteristics and syllabic duty factor.

Conversational CW, uses a 40% duty cycle

Voice FM, RTTY, FT8 use a 100% duty cycle.

--- 

What we don't know from the simple "heat resistance" model are the time constants involved,

How quick do things heat up and cool down?

I never learnt this at University, never saw it discussed in my textbooks and when I asked a couple of mechanical engineering university lecturers about it, they didn't know either, Other than a cursory mention of thermal mass and their specific capacities. I put the calculation to one side as the one time I needed it I did a crude calculation and actually measured a time graph of the temperature rise, close enough in my specific case. I always thought that I must get back to that and research how to do it "properly".

Then along came a Youtube video by "FesZ",  Riccardo Tinivella entitled "Static and Transient Thermal Models", you can find it at  https://youtu.be/BGi_n28D8ro?si=CAK3EodXQ0U-wn-K 

For the simple static cases of above, FesZ uses resistors and adds a voltage source to model ambient temperature and a current source to represent the source of energy (heat). Voltage is an analogy for temperature and the amps represents watts in the current source.

Even this "model" is useful if there are several sources of heat, the "circuit" then has parallel and series resistance between the several current sources and the one voltage source. You can see what each device case temperature gets to. LTspice does the calculation for you. 

For the single device described above I made three models.

First I modelled the equation above and then added the case to air dissipation (R8), I then adjusted the given 62 degrees/watt to twice this value, reasoning that only half the case is exposed to the air and half connected to the heatsink.


So junction temperatures ranged from 155, 133 and 143 degrees for 10 Watts of dissipation, 100% of the time.

If we had two IRF510s, each dissipating 5 watts then the circuit, and corresponding temperatures would be as below;  the junctions are now at just under 120 degrees. The heatsinks are a bit hot however.


If we replace the heat sink with something around 5 degrees/watt then the temperatures drop to

; with Heat sink of 5 degrees/watt
V(heatsink_temp3):        70
V(junction_temp3):        88
V(device_case_temp4):  73
V(ambient_temp3):         25
V(device_case_temp3):   73
V(junction_temp4):         88

Still a bit hot, try to get your heatsink down to 50, and keep an eye on junction temperature.

For an SSB transmitter that is transmitting uncompressed, unprocessed speech we can use 30% of peak power. Simply setting the current to 3 Amps (1.5A per device) gives us a thermal model for 10W PEP/3W average. The 3.5 degrees/watt heatsink is at 35 degrees and the junctions are at 40.

To really see the changes of temperature when pulses of power are applied, the dynamic changes  of temperature requires knowing the heat capacity, the thermal masses of the devices, this is rarely given in the datasheets but FesZ shows devices that summarize the data and this is enough to model and produce graphs of temperature against time.

Some transistors have thermal models that are either a Pi network of series resistors and capacitors to ground or a ladder network of parallel RC pairs in series. Inserting this in the thermal models and using a pulse or changing current source allows plotting the peak temperature against time. You could even apply a .WAV file of speech to the current source. See FesZ video for a good example.

Two problems remain, sometimes the data is missing or given in an alternative form. and secondly the environment around the heatsink matters a lot. A heatsink contained within a small case or fed an airflow from a fan changes the figures dramatically. This is why heatsinks are often mounted outside the case but you must allow clear space above and below the heatsink so that convection can take place and the heated air rises away from the heatsink and ambient temperature air is drawn in. 

You could use thermal data from a similar device in the same case/package. Better than nothing, but design conservatively. 

As regards fans, there are graphs for some heatsink datasheets that give improved thermal resistance values for various flow rates. Again, assuming the fan is fed cool ambient air and pushes the hot air away. 

The volume of a heatsink for a given flow condition can be obtained by using the following equation:

 Volume(heatsink) = volumetric resistance (Cm3 °C/W)/thermal resistance θSA (°C/W) 

An approximate range of volumetric resistance is given in the following table: (TI datasheet SLVA462)

Available Airflow Volumetric Resistance
 (LFM)                    (Cm3 °C/W)
 200                            150 - 250 
 500                              80 - 150
 1000                            50 - 80 
 NC                            500 – 800

The next important criterion for the performance of a heatsink is the width. It is linearly proportional to the performance of the heatsink in the direction perpendicular to the airflow. Considering an example, an increase in the width of a heatsink by a factor of two, three, or four increase the heat dissipation capability by a factor of two, three, or four.

Similarly, the square root of the fin length used is approximately proportional to the performance of the heatsink in the direction parallel to the airflow. In case of an increase in the length of the heatsink by a factor of two, three, or four only increases the heat dissipation capability by a factor of 1.4, 1.7, or 2. 

If the board has sufficient space, it is always beneficial to increase the width of a heatsink rather than the length of the heatsink.

The full calculation for the combined thermal resistance of a fan and heatsink is so complex you are better to use an online calculator or apply some simple rules of thumb.

A (over)simple rule of thumb is that a fan will decrease the thermal resistance of a heatsink by a factor of two to four. But there are a lot of "it depends" things to think about.

The maths behind some of it is described at https://www.heatsinkcalculator.com/blog/heat-sink-design-optimization-for-forced-convection/ if you enjoy working with hyperbolic Tan functions and Reynold numbers then knock yourself out...

The same website covers using PCB as a heatsink, which is often overlooked but is important particularly when using surface mount components. (https://www.heatsinkcalculator.com/blog/how-to-calculate-the-thermal-resistance-of-a-pcb/) these uses Bessel functions, wild maths...

A much more sophisticated approach is to use proper thermal modelling software that uses "fine white elephants" to quote an in-phrase we used to confuse students with. FEM, (Finite Element Method) analysis breaks down a 3D shape into small sections and uses boundary conditions to create matrices to be solved by numerical methods. Often used for calculating and visually displaying mechanical stress or electromagnetic field strengths, it can also solve dynamic (and static) thermal flow problems.

FreeCAD has an FEM workbench although I have not yet gone down that rabbit hole, curiouser and curiouser...